diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 08edd8f36da..06b114f2c5c 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -42,7 +42,7 @@ jobs: persist-credentials: false - name: Setup uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # ratchet:astral-sh/setup-uv@v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # ratchet:astral-sh/setup-uv@v9.0.0 with: version: "0.11.25" enable-cache: false diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 06d9790c72e..3b3db477c5f 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -46,7 +46,7 @@ jobs: fetch-tags: true - name: Setup uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # ratchet:astral-sh/setup-uv@v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # ratchet:astral-sh/setup-uv@v9.0.0 with: version: "0.11.25" enable-cache: false @@ -169,7 +169,7 @@ jobs: persist-credentials: false - name: Setup uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # ratchet:astral-sh/setup-uv@v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # ratchet:astral-sh/setup-uv@v9.0.0 with: version: "0.11.25" enable-cache: false @@ -198,7 +198,7 @@ jobs: fetch-depth: 0 - name: Setup uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # ratchet:astral-sh/setup-uv@v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # ratchet:astral-sh/setup-uv@v9.0.0 with: version: "0.11.25" # NOTE: This isn't caching much and zizmor suggests this could be poisoned, so disable. @@ -2027,7 +2027,7 @@ jobs: - name: Setup uv if: steps.should-run.outputs.run == 'true' - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # ratchet:astral-sh/setup-uv@v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # ratchet:astral-sh/setup-uv@v9.0.0 with: version: "0.11.25" enable-cache: false diff --git a/.github/workflows/helm-chart-releases.yml b/.github/workflows/helm-chart-releases.yml index ac8b7b2d2ca..3fa4e406e6a 100644 --- a/.github/workflows/helm-chart-releases.yml +++ b/.github/workflows/helm-chart-releases.yml @@ -25,7 +25,7 @@ jobs: persist-credentials: false - name: Install Helm CLI - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # ratchet:azure/setup-helm@v5.0.0 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # ratchet:azure/setup-helm@v5.0.1 with: version: v3.12.1 diff --git a/.github/workflows/post-merge-beta-cherry-pick.yml b/.github/workflows/post-merge-beta-cherry-pick.yml index d7237e47c29..0a1b3199cab 100644 --- a/.github/workflows/post-merge-beta-cherry-pick.yml +++ b/.github/workflows/post-merge-beta-cherry-pick.yml @@ -155,7 +155,7 @@ jobs: token: ${{ steps.app-token.outputs.token }} - name: Install the latest version of uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 with: enable-cache: false version: "0.11.25" diff --git a/.github/workflows/pr-external-dependency-unit-tests.yml b/.github/workflows/pr-external-dependency-unit-tests.yml index 8815f99ddc4..3787e2d10e5 100644 --- a/.github/workflows/pr-external-dependency-unit-tests.yml +++ b/.github/workflows/pr-external-dependency-unit-tests.yml @@ -119,7 +119,7 @@ jobs: - name: Setup Helm if: matrix.test-dir == 'craft_helm' - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # ratchet:azure/setup-helm@v5.0.0 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # ratchet:azure/setup-helm@v5.0.1 with: version: v3.19.0 diff --git a/.github/workflows/pr-helm-chart-testing.yml b/.github/workflows/pr-helm-chart-testing.yml index c8448be7fd0..ba3556a10d8 100644 --- a/.github/workflows/pr-helm-chart-testing.yml +++ b/.github/workflows/pr-helm-chart-testing.yml @@ -35,7 +35,7 @@ jobs: persist-credentials: false - name: Set up Helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # ratchet:azure/setup-helm@v5.0.0 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # ratchet:azure/setup-helm@v5.0.1 with: version: v3.19.0 diff --git a/.github/workflows/pr-playwright-tests.yml b/.github/workflows/pr-playwright-tests.yml index 5a8fb9feefa..fb7f9581b3b 100644 --- a/.github/workflows/pr-playwright-tests.yml +++ b/.github/workflows/pr-playwright-tests.yml @@ -205,6 +205,8 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} + # SKIP_TYPE_CHECK cuts the build time of this image. Types are still checked + # by the `typescript-check` prek hook in the Quality Checks PR workflow. - name: Build and push Web Docker image uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: @@ -215,6 +217,7 @@ jobs: push: true build-args: | BASE_IMAGE_REGISTRY=${{ env.BASE_IMAGE_REGISTRY }} + SKIP_TYPE_CHECK=1 cache-from: | type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:web-cache-${{ github.event.pull_request.head.sha || github.sha }} type=registry,ref=${{ env.RUNS_ON_ECR_CACHE }}:web-cache-${{ steps.format-branch.outputs.cache-suffix }} @@ -988,7 +991,7 @@ jobs: aws-region: us-east-2 - name: Install the latest version of uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # ratchet:astral-sh/setup-uv@v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # ratchet:astral-sh/setup-uv@v9.0.0 with: enable-cache: false version: "0.11.25" diff --git a/.github/workflows/pr-quality-checks.yml b/.github/workflows/pr-quality-checks.yml index 5c1ea09d440..e954d7ead8d 100644 --- a/.github/workflows/pr-quality-checks.yml +++ b/.github/workflows/pr-quality-checks.yml @@ -24,7 +24,7 @@ jobs: with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # ratchet:actions/setup-python@v6 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # ratchet:actions/setup-python@v7.0.0 with: python-version: "3.13" - name: Setup Terraform diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index d3eec417e18..5e251aef191 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v6 with: persist-credentials: false - - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # ratchet:astral-sh/setup-uv@v8.2.0 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # ratchet:astral-sh/setup-uv@v9.0.0 with: enable-cache: false version: "0.11.25" diff --git a/.github/workflows/release-devtools.yml b/.github/workflows/release-devtools.yml index 2b741a7bb38..aa4532a7cd6 100644 --- a/.github/workflows/release-devtools.yml +++ b/.github/workflows/release-devtools.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v6 with: persist-credentials: false - - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # ratchet:astral-sh/setup-uv@v8.2.0 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # ratchet:astral-sh/setup-uv@v9.0.0 with: enable-cache: false version: "0.11.25" @@ -74,7 +74,7 @@ jobs: token: ${{ steps.app-token.outputs.token }} - name: Install the latest version of uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # ratchet:astral-sh/setup-uv@v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # ratchet:astral-sh/setup-uv@v9.0.0 with: enable-cache: false version: "0.11.25" diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 9c592251a1c..aff351bab0c 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -25,7 +25,7 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # ratchet:astral-sh/setup-uv@v8.2.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # ratchet:astral-sh/setup-uv@v9.0.0 with: enable-cache: false version: "0.11.25" diff --git a/backend/ee/onyx/external_permissions/sharepoint/permission_utils.py b/backend/ee/onyx/external_permissions/sharepoint/permission_utils.py index 60882c5e0cd..961e83f6539 100644 --- a/backend/ee/onyx/external_permissions/sharepoint/permission_utils.py +++ b/backend/ee/onyx/external_permissions/sharepoint/permission_utils.py @@ -27,7 +27,9 @@ from onyx.connectors.sharepoint.connector import ( GRAPH_API_MAX_RETRIES, GRAPH_API_RETRYABLE_STATUSES, + LIST_ITEM_ID_PROPERTY, SHARED_DOCUMENTS_MAP_REVERSE, + SHAREPOINT_IDS_PROPERTY, sleep_and_retry, ) from onyx.connectors.sharepoint.connector_utils import ( @@ -50,6 +52,7 @@ MICROSOFT_DOMAIN = ".onmicrosoft" SHAREPOINT_GROUP_SCOPE_SEPARATOR = "::" GROUP_CACHE_KEY_SEPARATOR = ":" +GET_SHAREPOINT_LIST_ITEM_ID_LABEL = "get_sharepoint_list_item_id" # PnP RoleType defines Guest=1 and RestrictedGuest=9: # https://github.com/pnp/pnpcore/blob/4e4f58fcac797f2957bfcd14fedcecd690dfe7ee/src/sdk/PnP.Core/Model/SharePoint/Core/Public/Enums/RoleType.cs LIMITED_ACCESS_ROLE_TYPES = frozenset({1, 9}) @@ -224,24 +227,20 @@ def _get_group_guid_from_identifier( def _get_sharepoint_list_item_id(drive_item: DriveItem) -> str | None: try: - # First try to get the list item directly from the drive item + properties = getattr(drive_item, "properties", None) + sharepoint_ids = properties.get(SHAREPOINT_IDS_PROPERTY) if properties else None + if isinstance(sharepoint_ids, dict): + if list_item_id := sharepoint_ids.get(LIST_ITEM_ID_PROPERTY): + return str(list_item_id) + if hasattr(drive_item, "listItem"): list_item = drive_item.listItem if list_item: - # Load the list item properties to get the ID - sleep_and_retry(list_item.get(), "get_sharepoint_list_item_id") + sleep_and_retry(list_item.get(), GET_SHAREPOINT_LIST_ITEM_ID_LABEL) if hasattr(list_item, "id") and list_item.id: return str(list_item.id) - # The SharePoint list item ID is typically available in the sharepointIds property - sharepoint_ids = getattr(drive_item, "sharepoint_ids", None) - if sharepoint_ids and hasattr(sharepoint_ids, "listItemId"): - return sharepoint_ids.listItemId - - # Alternative: try to get it from the properties - properties = getattr(drive_item, "properties", None) if properties: - # Sometimes the SharePoint list item ID is in the properties for prop_name, prop_value in properties.items(): if "listitemid" in prop_name.lower(): return str(prop_value) diff --git a/backend/ee/onyx/external_permissions/slack/channel_access.py b/backend/ee/onyx/external_permissions/slack/channel_access.py index 26db8289844..a0305ce3515 100644 --- a/backend/ee/onyx/external_permissions/slack/channel_access.py +++ b/backend/ee/onyx/external_permissions/slack/channel_access.py @@ -1,16 +1,12 @@ -from slack_sdk import WebClient - from onyx.access.models import ExternalAccess from onyx.connectors.models import BasicExpertInfo from onyx.connectors.slack.connector import ChannelType, channel_team_ids -from onyx.connectors.slack.utils import ( - expert_info_from_slack_id, - make_paginated_slack_api_call, -) +from onyx.connectors.slack.source_operations import SlackSourceOperations +from onyx.connectors.slack.utils import expert_info_from_slack_id def get_channel_access( - client: WebClient, + slack_client: SlackSourceOperations, channel: ChannelType, user_cache: dict[str, BasicExpertInfo | None], team_id_to_user_emails: dict[str, set[str]] | None = None, @@ -45,17 +41,14 @@ def get_channel_access( channel_id = channel["id"] member_ids = [] - for result in make_paginated_slack_api_call( - client.conversations_members, - channel=channel_id, - ): - member_ids.extend(result.get("members", [])) + for result in slack_client.list_channel_members(channel_id=channel_id): + member_ids.extend(result.members) member_emails = set() for member_id in member_ids: user_info = expert_info_from_slack_id( user_id=member_id, - client=client, + fetch_user_info=slack_client.fetch_user_info, user_cache=user_cache, ) if user_info and user_info.email: diff --git a/backend/ee/onyx/external_permissions/slack/doc_sync.py b/backend/ee/onyx/external_permissions/slack/doc_sync.py index 995a79ce4bd..adbd40ec708 100644 --- a/backend/ee/onyx/external_permissions/slack/doc_sync.py +++ b/backend/ee/onyx/external_permissions/slack/doc_sync.py @@ -1,7 +1,5 @@ from collections.abc import Generator -from slack_sdk import WebClient - from ee.onyx.external_permissions.perm_sync_types import ( FetchAllDocumentsFunction, FetchAllDocumentsIdsFunction, @@ -11,9 +9,9 @@ fetch_team_user_emails, fetch_user_id_to_email_map, ) -from ee.onyx.external_permissions.utils import credential_json from onyx.access.models import DocExternalAccess, ExternalAccess -from onyx.connectors.credentials_provider import OnyxDBCredentialsProvider +from onyx.configs.constants import DocumentSource +from onyx.connectors.credentials_provider import build_db_credentials_provider from onyx.connectors.interfaces import SecondsSinceUnixEpoch from onyx.connectors.models import HierarchyNode from onyx.connectors.slack.connector import ( @@ -22,14 +20,12 @@ get_channels, get_channels_across_teams, list_grid_team_ids, - make_paginated_slack_api_call, ) from onyx.connectors.slack.models import ChannelType +from onyx.connectors.slack.source_operations import SlackSourceOperations from onyx.db.models import ConnectorCredentialPair from onyx.indexing.indexing_heartbeat import IndexingHeartbeatInterface -from onyx.redis.redis_pool import get_redis_client from onyx.utils.logger import setup_logger -from shared_configs.contextvars import get_current_tenant_id logger = setup_logger() @@ -87,7 +83,7 @@ def _filter_channels_for_permissions( def _fetch_channel_permissions( - slack_client: WebClient, + slack_client: SlackSourceOperations, workspace_permissions: ExternalAccess, # noqa: ARG001 user_id_to_email_map: dict[str, str], team_ids: list[str] | None = None, @@ -100,25 +96,25 @@ def _fetch_channel_permissions( channel_permissions = {} if team_ids: public_channels = get_channels_across_teams( - client=slack_client, + slack_client=slack_client, team_ids=team_ids, get_public=True, get_private=False, ) private_channels = get_channels_across_teams( - client=slack_client, + slack_client=slack_client, team_ids=team_ids, get_public=False, get_private=True, ) else: public_channels = get_channels( - client=slack_client, + slack_client=slack_client, get_public=True, get_private=False, ) private_channels = get_channels( - client=slack_client, + slack_client=slack_client, get_public=False, get_private=True, ) @@ -142,7 +138,7 @@ def _fetch_channel_permissions( continue if team_id_to_user_emails: channel_permissions[channel_id] = get_channel_access( - client=slack_client, + slack_client=slack_client, channel=channel, user_cache={}, team_id_to_user_emails=team_id_to_user_emails, @@ -158,11 +154,8 @@ def _fetch_channel_permissions( for channel_id in private_channel_ids: # Collect all member ids for the channel pagination calls member_ids = [] - for result in make_paginated_slack_api_call( - slack_client.conversations_members, - channel=channel_id, - ): - member_ids.extend(result.get("members", [])) + for result in slack_client.list_channel_members(channel_id=channel_id): + member_ids.extend(result.members) # Collect all member emails for the channel member_emails = set() @@ -173,8 +166,8 @@ def _fetch_channel_permissions( # If the user is an external user, they wont get returned from the # conversations_members call so we need to make a separate call to users_info # and add them to the user_id_to_email_map - member_info = slack_client.users_info(user=member_id) - member_email = member_info["user"]["profile"].get("email") + member_info = slack_client.fetch_user_info(member_id) + member_email = member_info.user["profile"].get("email") if not member_email: # If no email is found, we skip the user continue @@ -241,30 +234,25 @@ def slack_doc_sync( callback: IndexingHeartbeatInterface | None, ) -> Generator[DocExternalAccess, None, None]: """ - Adds the external permissions to the documents in postgres - if the document doesn't already exists in postgres, we create - it in postgres so that when it gets created later, the permissions are - already populated + Adds the external permissions to the documents in postgres if the document + doesn't already exists in postgres, we create it in postgres so that when it + gets created later, the permissions are already populated """ - # Use credentials provider instead of directly loading credentials - - tenant_id = get_current_tenant_id() - provider = OnyxDBCredentialsProvider(tenant_id, "slack", cc_pair.credential.id) - r = get_redis_client(tenant_id=tenant_id) - creds = credential_json(cc_pair) - slack_client = SlackConnector.make_slack_web_client( - provider.get_provider_key(), - creds["slack_bot_token"], - SlackConnector.MAX_RETRIES, - r, + # One provider and one gateway-owned client, shared with the indexing path: + # the connector's rotation lock and rate-limit delay keys now coordinate + # with running indexing jobs instead of living under perm-sync-only keys. + provider = build_db_credentials_provider( + DocumentSource.SLACK, cc_pair.credential.id ) slack_connector = SlackConnector(**cc_pair.connector.connector_specific_config) slack_connector.set_credentials_provider(provider) + slack_client = slack_connector.slack_client + assert slack_client is not None, "set_credentials_provider builds the gateway." grid_team_ids: list[str] | None = None try: - auth_response = slack_client.auth_test() - if auth_response.get("enterprise_id"): + auth_response = slack_client.check_auth() + if auth_response.enterprise_id: grid_team_ids = list_grid_team_ids(slack_client) except Exception as e: logger.warning("Slack Grid detection during perm sync failed: %s", e) diff --git a/backend/ee/onyx/external_permissions/slack/group_sync.py b/backend/ee/onyx/external_permissions/slack/group_sync.py index d56028022fe..368986f259c 100644 --- a/backend/ee/onyx/external_permissions/slack/group_sync.py +++ b/backend/ee/onyx/external_permissions/slack/group_sync.py @@ -5,47 +5,43 @@ THERE IS NO USERGROUP <-> DOCUMENT PERMISSION MAPPING """ -from slack_sdk import WebClient - from ee.onyx.db.external_perm import ExternalUserGroup from ee.onyx.external_permissions.slack.utils import fetch_user_id_to_email_map -from ee.onyx.external_permissions.utils import credential_json -from onyx.connectors.credentials_provider import OnyxDBCredentialsProvider -from onyx.connectors.slack.connector import SlackConnector -from onyx.connectors.slack.utils import make_paginated_slack_api_call +from onyx.configs.constants import DocumentSource +from onyx.connectors.credentials_provider import build_db_credentials_provider +from onyx.connectors.slack.source_operations import SlackSourceOperations from onyx.db.models import ConnectorCredentialPair -from onyx.redis.redis_pool import get_redis_client from onyx.utils.logger import setup_logger logger = setup_logger() def _get_slack_group_ids( - slack_client: WebClient, + slack_client: SlackSourceOperations, ) -> list[str]: - group_ids = [] - for result in make_paginated_slack_api_call(slack_client.usergroups_list): - for group in result.get("usergroups", []): - group_ids.append(group.get("id")) + group_ids: list[str] = [] + for result in slack_client.list_usergroups(): + for group in result.usergroups: + group_id = group.get("id") + if group_id: + group_ids.append(group_id) return group_ids def _get_slack_group_members_email( - slack_client: WebClient, + slack_client: SlackSourceOperations, group_name: str, user_id_to_email_map: dict[str, str], ) -> list[str]: group_member_emails = [] - for result in make_paginated_slack_api_call( - slack_client.usergroups_users_list, usergroup=group_name - ): - for member_id in result.get("users", []): + for result in slack_client.list_usergroup_members(usergroup_id=group_name): + for member_id in result.users: member_email = user_id_to_email_map.get(member_id) if not member_email: # If the user is an external user, they wont get returned from the # conversations_members call so we need to make a separate call to users_info - member_info = slack_client.users_info(user=member_id) - member_email = member_info["user"]["profile"].get("email") + member_info = slack_client.fetch_user_info(member_id) + member_email = member_info.user["profile"].get("email") if not member_email: # If no email is found, we skip the user continue @@ -56,21 +52,17 @@ def _get_slack_group_members_email( def slack_group_sync( - tenant_id: str, + tenant_id: str, # noqa: ARG001 cc_pair: ConnectorCredentialPair, ) -> list[ExternalUserGroup]: """NOTE: not used atm. All channel access is done at the individual user level. Leaving in for now in case we need it later.""" - provider = OnyxDBCredentialsProvider(tenant_id, "slack", cc_pair.credential.id) - r = get_redis_client(tenant_id=tenant_id) - creds = credential_json(cc_pair) - slack_client = SlackConnector.make_slack_web_client( - provider.get_provider_key(), - creds["slack_bot_token"], - SlackConnector.MAX_RETRIES, - r, + # The provider derives the tenant from the request context, like doc sync. + provider = build_db_credentials_provider( + DocumentSource.SLACK, cc_pair.credential.id ) + slack_client = SlackSourceOperations(credentials_provider=provider) user_id_to_email_map = fetch_user_id_to_email_map(slack_client) diff --git a/backend/ee/onyx/external_permissions/slack/utils.py b/backend/ee/onyx/external_permissions/slack/utils.py index 669db89bd16..3edd6ad1d17 100644 --- a/backend/ee/onyx/external_permissions/slack/utils.py +++ b/backend/ee/onyx/external_permissions/slack/utils.py @@ -1,13 +1,11 @@ -from slack_sdk import WebClient - +from onyx.connectors.slack.source_operations import SlackSourceOperations from onyx.connectors.slack.utils import ( fetch_team_user_emails as _fetch_team_user_emails, ) -from onyx.connectors.slack.utils import make_paginated_slack_api_call def fetch_user_id_to_email_map( - slack_client: WebClient, + slack_client: SlackSourceOperations, team_ids: list[str] | None = None, ) -> dict[str, str]: """On Grid org installs, ``users.list`` requires a ``team_id``; iterate @@ -15,21 +13,17 @@ def fetch_user_id_to_email_map( user_id_to_email_map: dict[str, str] = {} team_iter: list[str | None] = list(team_ids) if team_ids else [None] for tid in team_iter: - kwargs: dict[str, str] = {} - if tid: - kwargs["team_id"] = tid - for user_info in make_paginated_slack_api_call( - slack_client.users_list, **kwargs - ): - for user in user_info.get("members", []): + for user_info in slack_client.list_users(team_id=tid): + for user in user_info.members: + user_id = user.get("id") email = user.get("profile", {}).get("email") - if email: - user_id_to_email_map[user.get("id")] = email + if user_id and email: + user_id_to_email_map[user_id] = email return user_id_to_email_map def fetch_team_user_emails( - slack_client: WebClient, + slack_client: SlackSourceOperations, team_ids: list[str], ) -> dict[str, set[str]]: """Re-export of ``onyx.connectors.slack.utils.fetch_team_user_emails`` for diff --git a/backend/ee/onyx/server/token_rate_limits/api.py b/backend/ee/onyx/server/token_rate_limits/api.py index 222c3d6febc..01247e224ed 100644 --- a/backend/ee/onyx/server/token_rate_limits/api.py +++ b/backend/ee/onyx/server/token_rate_limits/api.py @@ -15,8 +15,12 @@ from onyx.db.enums import Permission from onyx.db.models import User from onyx.db.token_limit import ( + delete_token_rate_limit, + fetch_all_global_token_rate_limits, fetch_all_user_token_rate_limits, + insert_global_token_rate_limit, insert_user_token_rate_limit, + update_token_rate_limit, ) from onyx.server.query_and_chat.token_limit import ( invalidate_any_rate_limit_exists_cache, @@ -24,11 +28,82 @@ from onyx.server.token_rate_limits.models import ( TokenRateLimitArgs, TokenRateLimitDisplay, + TokenRateLimitUpdateArgs, ) +# Spending limits are an Enterprise feature, so every endpoint here is mounted +# only by the EE app. Community builds have no way to create or edit a limit. router = APIRouter(prefix="/admin/token-rate-limits", tags=PUBLIC_API_TAGS) +""" +Global Token Limit Settings +""" + + +@router.get("/global") +def get_global_token_limit_settings( + _: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)), + db_session: Session = Depends(get_session), +) -> list[TokenRateLimitDisplay]: + return [ + TokenRateLimitDisplay.from_db(token_rate_limit) + for token_rate_limit in fetch_all_global_token_rate_limits(db_session) + ] + + +@router.post("/global") +def create_global_token_limit_settings( + token_limit_settings: TokenRateLimitArgs, + _: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)), + db_session: Session = Depends(get_session), +) -> TokenRateLimitDisplay: + rate_limit_display = TokenRateLimitDisplay.from_db( + insert_global_token_rate_limit(db_session, token_limit_settings) + ) + # clear cache in case this was the first rate limit created + invalidate_any_rate_limit_exists_cache() + return rate_limit_display + + +""" +General Token Limit Settings +""" + + +@router.put("/rate-limit/{token_rate_limit_id}") +def update_token_limit_settings( + token_rate_limit_id: int, + token_limit_settings: TokenRateLimitUpdateArgs, + _: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)), + db_session: Session = Depends(get_session), +) -> TokenRateLimitDisplay: + rate_limit_display = TokenRateLimitDisplay.from_db( + update_token_rate_limit( + db_session=db_session, + token_rate_limit_id=token_rate_limit_id, + token_rate_limit_settings=token_limit_settings, + ) + ) + # enabling/disabling a limit changes whether any rate limit exists + invalidate_any_rate_limit_exists_cache() + return rate_limit_display + + +@router.delete("/rate-limit/{token_rate_limit_id}") +def delete_token_limit_settings( + token_rate_limit_id: int, + _: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)), + db_session: Session = Depends(get_session), +) -> None: + delete_token_rate_limit( + db_session=db_session, + token_rate_limit_id=token_rate_limit_id, + ) + # removing a limit can change whether any rate limit exists + invalidate_any_rate_limit_exists_cache() + + """ Group Token Limit Settings """ diff --git a/backend/onyx/connectors/highspot/client.py b/backend/onyx/connectors/highspot/client.py index f605f4a9d36..dc9d2049bab 100644 --- a/backend/onyx/connectors/highspot/client.py +++ b/backend/onyx/connectors/highspot/client.py @@ -161,7 +161,11 @@ def _make_request( if isinstance(error_data, dict): error_msg = error_data.get("message", str(e)) except (ValueError, KeyError): - pass + # Highspot sends some errors as plain text (e.g. the licensing + # 403). Keep the body so the reason reaches the logs. + body = e.response.text.strip() + if body: + error_msg = f"{e} - {body}" if status_code == 401: raise HighspotAuthenticationError(f"Authentication failed: {error_msg}") diff --git a/backend/onyx/connectors/sharepoint/connector.py b/backend/onyx/connectors/sharepoint/connector.py index 0f866f09d1d..a313d58a09e 100644 --- a/backend/onyx/connectors/sharepoint/connector.py +++ b/backend/onyx/connectors/sharepoint/connector.py @@ -156,6 +156,38 @@ def _build_item_relative_path(parent_reference_path: str | None, item_name: str) GRAPH_API_BASE = f"{DEFAULT_GRAPH_API_HOST}/v1.0" GRAPH_API_MAX_RETRIES = 5 GRAPH_API_RETRYABLE_STATUSES = frozenset({429, 500, 502, 503, 504}) +SHAREPOINT_IDS_PROPERTY = "sharepointIds" +LIST_ITEM_ID_PROPERTY = "listItemId" +DRIVE_ITEM_ID_PROPERTY = "id" +DRIVE_ITEM_NAME_PROPERTY = "name" +DRIVE_ITEM_WEB_URL_PROPERTY = "webUrl" +DRIVE_ITEM_SIZE_PROPERTY = "size" +DRIVE_ITEM_FILE_PROPERTY = "file" +DRIVE_ITEM_CREATED_DATETIME_PROPERTY = "createdDateTime" +DRIVE_ITEM_LAST_MODIFIED_DATETIME_PROPERTY = "lastModifiedDateTime" +DRIVE_ITEM_LAST_MODIFIED_BY_PROPERTY = "lastModifiedBy" +DRIVE_ITEM_PARENT_REFERENCE_PROPERTY = "parentReference" +DRIVE_ITEM_FOLDER_PROPERTY = "folder" +DRIVE_ITEM_DELETED_PROPERTY = "deleted" +DRIVE_ITEM_DOWNLOAD_URL_PROPERTY = "@microsoft.graph.downloadUrl" +DRIVE_ITEM_DOWNLOAD_URL_SELECT = "content.downloadUrl" +DRIVE_ITEM_SELECT_FIELDS = ",".join( + ( + DRIVE_ITEM_ID_PROPERTY, + DRIVE_ITEM_NAME_PROPERTY, + DRIVE_ITEM_WEB_URL_PROPERTY, + DRIVE_ITEM_SIZE_PROPERTY, + DRIVE_ITEM_FILE_PROPERTY, + DRIVE_ITEM_CREATED_DATETIME_PROPERTY, + DRIVE_ITEM_LAST_MODIFIED_DATETIME_PROPERTY, + DRIVE_ITEM_LAST_MODIFIED_BY_PROPERTY, + DRIVE_ITEM_PARENT_REFERENCE_PROPERTY, + SHAREPOINT_IDS_PROPERTY, + DRIVE_ITEM_FOLDER_PROPERTY, + DRIVE_ITEM_DELETED_PROPERTY, + DRIVE_ITEM_DOWNLOAD_URL_SELECT, + ) +) # Cap how many configured sites the perm-sync RoleAssignments probe checks at # validation time. Each probe is one HTTP round-trip, so we trade exhaustive @@ -183,29 +215,33 @@ class DriveItemData(BaseModel): last_modified_by_email: str | None = None parent_reference_path: str | None = None drive_id: str | None = None + list_item_id: str | None = None @classmethod def from_graph_json(cls, item: dict[str, Any]) -> "DriveItemData": - last_mod_raw = item.get("lastModifiedDateTime") + last_mod_raw = item.get(DRIVE_ITEM_LAST_MODIFIED_DATETIME_PROPERTY) last_mod: datetime | None = None if isinstance(last_mod_raw, str): last_mod = datetime.fromisoformat(last_mod_raw.replace("Z", "+00:00")) - created_raw = item.get("createdDateTime") + created_raw = item.get(DRIVE_ITEM_CREATED_DATETIME_PROPERTY) created: datetime | None = None if isinstance(created_raw, str): created = datetime.fromisoformat(created_raw.replace("Z", "+00:00")) - last_modified_by = item.get("lastModifiedBy", {}).get("user", {}) - parent_ref = item.get("parentReference", {}) + last_modified_by = item.get(DRIVE_ITEM_LAST_MODIFIED_BY_PROPERTY, {}).get( + "user", {} + ) + parent_ref = item.get(DRIVE_ITEM_PARENT_REFERENCE_PROPERTY, {}) + sharepoint_ids = item.get(SHAREPOINT_IDS_PROPERTY) or {} return cls( - id=item["id"], - name=item.get("name", ""), - web_url=item.get("webUrl", ""), - size=item.get("size"), - mime_type=item.get("file", {}).get("mimeType"), - download_url=item.get("@microsoft.graph.downloadUrl"), + id=item[DRIVE_ITEM_ID_PROPERTY], + name=item.get(DRIVE_ITEM_NAME_PROPERTY, ""), + web_url=item.get(DRIVE_ITEM_WEB_URL_PROPERTY, ""), + size=item.get(DRIVE_ITEM_SIZE_PROPERTY), + mime_type=item.get(DRIVE_ITEM_FILE_PROPERTY, {}).get("mimeType"), + download_url=item.get(DRIVE_ITEM_DOWNLOAD_URL_PROPERTY), created_datetime=created, last_modified_datetime=last_mod, last_modified_by_display_name=last_modified_by.get("displayName"), @@ -215,6 +251,7 @@ def from_graph_json(cls, item: dict[str, Any]) -> "DriveItemData": ), parent_reference_path=parent_ref.get("path"), drive_id=parent_ref.get("driveId"), + list_item_id=sharepoint_ids.get(LIST_ITEM_ID_PROPERTY), ) def to_sdk_driveitem(self, graph_client: GraphClient) -> DriveItem: @@ -227,6 +264,11 @@ def to_sdk_driveitem(self, graph_client: GraphClient) -> DriveItem: ) item = DriveItem(graph_client, path) item.set_property("id", self.id) + if self.list_item_id: + item.set_property( + SHAREPOINT_IDS_PROPERTY, + {LIST_ITEM_ID_PROPERTY: self.list_item_id}, + ) return item @@ -2050,15 +2092,20 @@ def _iter_drive_items_paged( while folder_queue: page_url: str | None = folder_queue.popleft() - params: dict[str, str] | None = {"$top": str(page_size)} + params: dict[str, str] | None = { + "$top": str(page_size), + "$select": DRIVE_ITEM_SELECT_FIELDS, + } while page_url: data = self._graph_api_get_json(page_url, params) params = None # nextLink already embeds query params for item in data.get("value", []): - if "folder" in item: - child_url = f"{base}/items/{item['id']}/children" + if DRIVE_ITEM_FOLDER_PROPERTY in item: + child_url = ( + f"{base}/items/{item[DRIVE_ITEM_ID_PROPERTY]}/children" + ) folder_queue.append(child_url) continue @@ -2069,7 +2116,7 @@ def _iter_drive_items_paged( # NOTE: We are now including items without a lastModifiedDateTime, # and respecting when only one of start or end is set. if start is not None or end is not None: - raw_ts = item.get("lastModifiedDateTime") + raw_ts = item.get(DRIVE_ITEM_LAST_MODIFIED_DATETIME_PROPERTY) if raw_ts: mod_dt = datetime.fromisoformat( raw_ts.replace("Z", "+00:00") @@ -2130,7 +2177,10 @@ def _iter_delta_pages( restarts with a full delta enumeration. """ page_url: str | None = initial_url - params: dict[str, str] | None = {"$top": str(page_size)} + params: dict[str, str] | None = { + "$top": str(page_size), + "$select": DRIVE_ITEM_SELECT_FIELDS, + } while page_url: try: @@ -2158,11 +2208,14 @@ def _iter_delta_pages( params = None # nextLink/deltaLink already embed query params for item in data.get("value", []): - if "folder" in item or "deleted" in item: + if ( + DRIVE_ITEM_FOLDER_PROPERTY in item + or DRIVE_ITEM_DELETED_PROPERTY in item + ): continue if start is not None or end is not None: - raw_ts = item.get("lastModifiedDateTime") + raw_ts = item.get(DRIVE_ITEM_LAST_MODIFIED_DATETIME_PROPERTY) if raw_ts: mod_dt = datetime.fromisoformat(raw_ts.replace("Z", "+00:00")) if start is not None and mod_dt < start: @@ -2184,12 +2237,14 @@ def _build_delta_start_url( ) -> str: """Build the initial delta API URL with query parameters embedded. - Embeds ``$top`` (and optionally a timestamp ``token``) directly in the - URL so that the returned string is fully self-contained and can be - stored in a checkpoint without needing a separate params dict. + Embeds ``$top``, ``$select``, and optionally ``token`` so the URL can be + stored in a checkpoint without a separate params dict. """ base_url = f"{self.graph_api_base}/drives/{drive_id}/root/delta" - params = [f"$top={page_size}"] + params = [ + f"$top={page_size}", + f"$select={DRIVE_ITEM_SELECT_FIELDS}", + ] if start is not None and start > _EPOCH: token = quote(start.isoformat(timespec="seconds")) params.append(f"token={token}") @@ -2220,16 +2275,22 @@ def _fetch_one_delta_page( "Delta token expired (410 Gone) for drive '%s'. Will restart with full delta enumeration.", drive_id, ) - full_url = f"{self.graph_api_base}/drives/{drive_id}/root/delta?$top={page_size}" + full_url = ( + f"{self.graph_api_base}/drives/{drive_id}/root/delta?" + f"$top={page_size}&$select={DRIVE_ITEM_SELECT_FIELDS}" + ) return [], full_url raise items: list[DriveItemData] = [] for item in data.get("value", []): - if "folder" in item or "deleted" in item: + if ( + DRIVE_ITEM_FOLDER_PROPERTY in item + or DRIVE_ITEM_DELETED_PROPERTY in item + ): continue if start is not None or end is not None: - raw_ts = item.get("lastModifiedDateTime") + raw_ts = item.get(DRIVE_ITEM_LAST_MODIFIED_DATETIME_PROPERTY) if raw_ts: mod_dt = datetime.fromisoformat(raw_ts.replace("Z", "+00:00")) if start is not None and mod_dt < start: diff --git a/backend/onyx/connectors/slack/access.py b/backend/onyx/connectors/slack/access.py index f9bb2fda30b..273a219bbc5 100644 --- a/backend/onyx/connectors/slack/access.py +++ b/backend/onyx/connectors/slack/access.py @@ -1,11 +1,10 @@ from collections.abc import Callable from typing import cast -from slack_sdk import WebClient - from onyx.access.models import ExternalAccess from onyx.connectors.models import BasicExpertInfo from onyx.connectors.slack.models import ChannelType +from onyx.connectors.slack.source_operations import SlackSourceOperations from onyx.utils.variable_functionality import ( fetch_versioned_implementation, global_version, @@ -13,7 +12,7 @@ def get_channel_access( - client: WebClient, + slack_client: SlackSourceOperations, channel: ChannelType, user_cache: dict[str, BasicExpertInfo | None], team_id_to_user_emails: dict[str, set[str]] | None = None, @@ -30,7 +29,7 @@ def get_channel_access( ee_get_channel_access = cast( Callable[ [ - WebClient, + SlackSourceOperations, ChannelType, dict[str, BasicExpertInfo | None], dict[str, set[str]] | None, @@ -42,4 +41,6 @@ def get_channel_access( ), ) - return ee_get_channel_access(client, channel, user_cache, team_id_to_user_emails) + return ee_get_channel_access( + slack_client, channel, user_cache, team_id_to_user_emails + ) diff --git a/backend/onyx/connectors/slack/connector.py b/backend/onyx/connectors/slack/connector.py index 65b29be0471..10334021a76 100644 --- a/backend/onyx/connectors/slack/connector.py +++ b/backend/onyx/connectors/slack/connector.py @@ -7,18 +7,10 @@ from concurrent.futures import Future, ThreadPoolExecutor, as_completed from datetime import datetime, timezone from enum import Enum -from http.client import IncompleteRead, RemoteDisconnected from typing import Any, cast -from urllib.error import URLError from urllib.parse import urlparse from pydantic import BaseModel, ConfigDict -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError -from slack_sdk.http_retry import ConnectionErrorRetryHandler, RetryHandler -from slack_sdk.http_retry.builtin_interval_calculators import ( - FixedValueRetryIntervalCalculator, -) from typing_extensions import override from onyx.access.models import ExternalAccess @@ -58,19 +50,19 @@ ) from onyx.connectors.slack.access import get_channel_access from onyx.connectors.slack.models import ChannelType, MessageType, ThreadType -from onyx.connectors.slack.onyx_retry_handler import OnyxRedisSlackRetryHandler -from onyx.connectors.slack.onyx_slack_web_client import OnyxSlackWebClient +from onyx.connectors.slack.source_operations import ( + SlackApiError, + SlackChannelVariant, + SlackSourceOperations, +) from onyx.connectors.slack.utils import ( SlackTextCleaner, expert_info_from_slack_id, fetch_team_user_emails, get_message_link, - make_paginated_slack_api_call, ) from onyx.db.enums import HierarchyNodeType from onyx.indexing.indexing_heartbeat import IndexingHeartbeatInterface -from onyx.redis.redis_pool import get_redis_client -from onyx.redis.tenant_redis_client import TenantRedisClient from onyx.utils.logger import setup_logger from onyx.utils.retry_after import parse_retry_after_seconds @@ -95,29 +87,33 @@ class SlackCheckpoint(ConnectorCheckpoint): def _collect_paginated_channels( - client: WebClient, + slack_client: SlackSourceOperations, exclude_archived: bool, channel_types: list[str], team_id: str | None = None, ) -> list[ChannelType]: channels: list[ChannelType] = [] - extra_kwargs: dict[str, Any] = {} - if team_id is not None: - extra_kwargs["team_id"] = team_id - for result in make_paginated_slack_api_call( - client.conversations_list, - exclude_archived=exclude_archived, + # The variant names the permission class of the call: any request that + # includes private channels needs ``groups:read``. + variant = ( + SlackChannelVariant.PRIVATE + if "private_channel" in channel_types + else SlackChannelVariant.PUBLIC + ) + for result in slack_client.list_channels( + variant=variant, # also get private channels the bot is added to - types=channel_types, - **extra_kwargs, + channel_types=channel_types, + exclude_archived=exclude_archived, + team_id=team_id, ): - channels.extend(result["channels"]) + channels.extend(cast(list[ChannelType], result.channels)) return channels def get_channels( - client: WebClient, + slack_client: SlackSourceOperations, exclude_archived: bool = True, get_public: bool = True, get_private: bool = True, @@ -133,7 +129,7 @@ def get_channels( # Try fetching both public and private channels first: try: channels = _collect_paginated_channels( - client=client, + slack_client=slack_client, exclude_archived=exclude_archived, channel_types=channel_types, team_id=team_id, @@ -147,7 +143,7 @@ def get_channels( logger.warning(msg + " Trying again with public channels only.") channel_types = ["public_channel"] channels = _collect_paginated_channels( - client=client, + slack_client=slack_client, exclude_archived=exclude_archived, channel_types=channel_types, team_id=team_id, @@ -155,33 +151,33 @@ def get_channels( return channels -def list_grid_team_ids(client: WebClient) -> list[str]: +def list_grid_team_ids(slack_client: SlackSourceOperations) -> list[str]: """Return Grid org team IDs via ``auth.teams.list`` (needs ``team:read``).""" team_ids: list[str] = [] - for result in make_paginated_slack_api_call(client.auth_teams_list): - for team in result.get("teams", []): + for result in slack_client.list_teams(): + for team in result.teams: team_id = team.get("id") if team_id: team_ids.append(team_id) return team_ids -def fetch_team_url(client: WebClient, team_id: str) -> str | None: +def fetch_team_url(slack_client: SlackSourceOperations, team_id: str) -> str | None: """Fetch a workspace URL for a given Grid team id via ``team.info``.""" try: - response = client.team_info(team=team_id) + response = slack_client.fetch_team_info(team_id=team_id) except SlackApiError as e: logger.warning( "team_info failed for team_id=%s: %s", team_id, e.response.get("error", "") ) return None - team = cast(dict[str, Any], response.get("team", {})) + team = response.team url = team.get("url") return cast(str, url) if isinstance(url, str) else None def get_channels_across_teams( - client: WebClient, + slack_client: SlackSourceOperations, team_ids: list[str], exclude_archived: bool = True, get_public: bool = True, @@ -192,7 +188,7 @@ def get_channels_across_teams( merged: list[ChannelType] = [] for team_id in team_ids: per_team = get_channels( - client=client, + slack_client=slack_client, exclude_archived=exclude_archived, get_public=get_public, get_private=get_private, @@ -209,8 +205,17 @@ def get_channels_across_teams( return merged +def _channel_history_variant(channel: ChannelType) -> SlackChannelVariant: + """The permission class of history/replies calls for this channel.""" + return ( + SlackChannelVariant.PRIVATE + if channel["is_private"] + else SlackChannelVariant.PUBLIC + ) + + def get_channel_messages( - client: WebClient, + slack_client: SlackSourceOperations, channel: ChannelType, oldest: str | None = None, latest: str | None = None, @@ -219,15 +224,12 @@ def get_channel_messages( """Get all messages in a channel""" # join so that the bot can access messages if not channel["is_member"]: - client.conversations_join( - channel=channel["id"], - is_private=channel["is_private"], - ) + slack_client.join_channel(channel_id=channel["id"]) logger.info("Successfully joined '%s'", channel["name"]) - for result in make_paginated_slack_api_call( - client.conversations_history, - channel=channel["id"], + for result in slack_client.fetch_channel_history( + variant=_channel_history_variant(channel), + channel_id=channel["id"], oldest=oldest, latest=latest, ): @@ -236,16 +238,20 @@ def get_channel_messages( raise RuntimeError("get_channel_messages: Stop signal detected") callback.progress("get_channel_messages", 0) - yield cast(list[MessageType], result["messages"]) + yield cast(list[MessageType], result.messages) -def get_thread(client: WebClient, channel_id: str, thread_id: str) -> ThreadType: +def get_thread( + slack_client: SlackSourceOperations, channel: ChannelType, thread_id: str +) -> ThreadType: """Get all messages in a thread""" threads: list[MessageType] = [] - for result in make_paginated_slack_api_call( - client.conversations_replies, channel=channel_id, ts=thread_id + for result in slack_client.fetch_thread_replies( + variant=_channel_history_variant(channel), + channel_id=channel["id"], + thread_ts=thread_id, ): - threads.extend(result["messages"]) + threads.extend(cast(list[MessageType], result.messages)) return threads @@ -278,7 +284,7 @@ def thread_to_doc( channel: ChannelType, thread: ThreadType, slack_cleaner: SlackTextCleaner, - client: WebClient, + slack_client: SlackSourceOperations, user_cache: dict[str, BasicExpertInfo | None], channel_access: ExternalAccess | None, team_id_to_url: dict[str, str] | None = None, @@ -287,7 +293,9 @@ def thread_to_doc( channel_team = _channel_team_id(channel) initial_sender_expert_info = expert_info_from_slack_id( - user_id=thread[0].get("user"), client=client, user_cache=user_cache + user_id=thread[0].get("user"), + fetch_user_info=slack_client.fetch_user_info, + user_cache=user_cache, ) initial_sender_name = ( initial_sender_expert_info.get_semantic_name() @@ -300,7 +308,9 @@ def thread_to_doc( all_sender_ids = [m.get("user") for m in thread] experts = [ expert_info_from_slack_id( - user_id=sender_id, client=client, user_cache=user_cache + user_id=sender_id, + fetch_user_info=slack_client.fetch_user_info, + user_cache=user_cache, ) for sender_id in all_sender_ids if sender_id @@ -326,7 +336,7 @@ def thread_to_doc( TextSection( link=get_message_link( event=m, - client=client, + slack_client=slack_client, channel_id=channel_id, team_id=channel_team, team_id_to_url=team_id_to_url, @@ -504,11 +514,13 @@ def _channel_to_hierarchy_node( ) -def _get_channel_by_id(client: WebClient, channel_id: str) -> ChannelType: +def _get_channel_by_id( + slack_client: SlackSourceOperations, channel_id: str +) -> ChannelType: """Get a channel by its ID. Args: - client: The Slack WebClient instance + slack_client: The Slack source-operations gateway channel_id: The ID of the channel to fetch Returns: @@ -517,15 +529,13 @@ def _get_channel_by_id(client: WebClient, channel_id: str) -> ChannelType: Raises: SlackApiError: If the channel cannot be fetched """ - response = client.conversations_info( - channel=channel_id, - ) - return cast(ChannelType, response["channel"]) + response = slack_client.fetch_channel_info(channel_id=channel_id) + return cast(ChannelType, response.channel) def _get_messages( channel: ChannelType, - client: WebClient, + slack_client: SlackSourceOperations, oldest: str | None = None, latest: str | None = None, limit: int = _SLACK_LIMIT, @@ -535,10 +545,7 @@ def _get_messages( # have to be in the channel in order to read messages if not channel["is_member"]: try: - client.conversations_join( - channel=channel["id"], - is_private=channel["is_private"], - ) + slack_client.join_channel(channel_id=channel["id"]) except SlackApiError as e: if e.response["error"] == "is_archived": logger.warning("Channel %s is archived. Skipping.", channel["name"]) @@ -548,26 +555,28 @@ def _get_messages( raise logger.info("Successfully joined '%s'", channel["name"]) - response = client.conversations_history( - channel=channel["id"], - oldest=oldest, - latest=latest, - limit=limit, + # Single page: the gateway paginator is lazy, so taking one yield makes + # exactly one request, and the page still carries the cursor metadata. + response = next( + slack_client.fetch_channel_history( + variant=_channel_history_variant(channel), + channel_id=channel["id"], + oldest=oldest, + latest=latest, + limit=limit, + ) ) - response.validate() - messages = cast(list[MessageType], response.get("messages", [])) + messages = cast(list[MessageType], response.messages) - cursor = cast(dict[str, Any], response.get("response_metadata", {})).get( - "next_cursor", "" - ) + cursor = response.response_metadata.next_cursor has_more = bool(cursor) return messages, has_more def _message_to_doc( message: MessageType, - client: WebClient, + slack_client: SlackSourceOperations, channel: ChannelType, slack_cleaner: SlackTextCleaner, user_cache: dict[str, BasicExpertInfo | None], @@ -594,7 +603,7 @@ def _message_to_doc( return None, None thread = get_thread( - client=client, channel_id=channel["id"], thread_id=thread_ts + slack_client=slack_client, channel=channel, thread_id=thread_ts ) # we'll just set and use the last filter reason if @@ -621,7 +630,7 @@ def _message_to_doc( channel=channel, thread=filtered_thread, slack_cleaner=slack_cleaner, - client=client, + slack_client=slack_client, user_cache=user_cache, channel_access=channel_access, team_id_to_url=team_id_to_url, @@ -630,7 +639,7 @@ def _message_to_doc( def _get_all_doc_ids( - client: WebClient, + slack_client: SlackSourceOperations, channels_to_include: list[str] | None = None, include_regex_enabled: bool = False, channels_to_exclude: list[str] | None = None, @@ -653,9 +662,11 @@ def _get_all_doc_ids( """ if team_ids: - all_channels = get_channels_across_teams(client=client, team_ids=team_ids) + all_channels = get_channels_across_teams( + slack_client=slack_client, team_ids=team_ids + ) else: - all_channels = get_channels(client) + all_channels = get_channels(slack_client) filtered_channels = filter_channels( all_channels, channels_to_include, @@ -670,7 +681,7 @@ def _get_all_doc_ids( # NOTE: external_access is a frozen object, so it's okay to safe to use a single # instance for all documents in the channel external_access = get_channel_access( - client=client, + slack_client=slack_client, channel=channel, user_cache=user_cache, team_id_to_user_emails=team_id_to_user_emails, @@ -687,7 +698,7 @@ def _get_all_doc_ids( ] channel_message_batches = get_channel_messages( - client=client, + slack_client=slack_client, channel=channel, callback=callback, oldest=str(start) if start else None, # 0.0 -> None intentionally @@ -737,7 +748,7 @@ class ProcessedSlackMessage(BaseModel): def _process_message( message: MessageType, - client: WebClient, + slack_client: SlackSourceOperations, channel: ChannelType, slack_cleaner: SlackTextCleaner, user_cache: dict[str, BasicExpertInfo | None], @@ -758,7 +769,7 @@ def _process_message( doc, filter_reason = _message_to_doc( message=message, - client=client, + slack_client=slack_client, channel=channel, slack_cleaner=slack_cleaner, user_cache=user_cache, @@ -786,7 +797,7 @@ def _process_message( ), document_link=get_message_link( message, - client, + slack_client, channel["id"], team_id=_channel_team_id(channel), team_id_to_url=team_id_to_url, @@ -815,10 +826,6 @@ class SlackConnector( CredentialsConnector, CheckpointedConnectorWithPermSync[SlackCheckpoint], ): - FAST_TIMEOUT = 1 - - MAX_RETRIES = 7 # arbitrarily selected - MAX_CHANNELS_TO_LOG = 50 # *** values to use when filtering bot channels *** @@ -858,19 +865,15 @@ def __init__( ) self.batch_size = batch_size self.num_threads = num_threads - self.client: WebClient | None = None - self.fast_client: WebClient | None = None + self.slack_client: SlackSourceOperations | None = None # just used for efficiency self.text_cleaner: SlackTextCleaner | None = None self.user_cache: dict[str, BasicExpertInfo | None] = {} self.credentials_provider: CredentialsProviderInterface | None = None - self.credential_prefix: str | None = None self.use_redis: bool = use_redis # None until _ensure_workspace_metadata() resolves it. self._workspace_metadata: _WorkspaceMetadata | None = None self._workspace_metadata_lock = threading.Lock() - # self.delay_lock: str | None = None # the redis key for the shared lock - # self.delay_key: str | None = None # the redis key for the shared delay @property def workspace_url(self) -> str | None: @@ -931,57 +934,6 @@ def normalize_url(cls, url: str) -> NormalizationResult: normalized = f"{channel_id}__{thread_ts}" return NormalizationResult(normalized_url=normalized, use_default=False) - @staticmethod - def make_credential_prefix(key: str) -> str: - return f"connector:slack:credential_{key}" - - @staticmethod - def make_delay_lock(prefix: str) -> str: - return f"{prefix}:delay_lock" - - @staticmethod - def make_delay_key(prefix: str) -> str: - return f"{prefix}:delay" - - @staticmethod - def make_slack_web_client( - prefix: str, token: str, max_retry_count: int, r: TenantRedisClient - ) -> WebClient: - delay_lock = SlackConnector.make_delay_lock(prefix) - delay_key = SlackConnector.make_delay_key(prefix) - - # NOTE: slack has a built in RateLimitErrorRetryHandler, but it isn't designed - # for concurrent workers. We've extended it with OnyxRedisSlackRetryHandler. - connection_error_retry_handler = ConnectionErrorRetryHandler( - max_retry_count=max_retry_count, - interval_calculator=FixedValueRetryIntervalCalculator(), - error_types=[ - URLError, - ConnectionResetError, - RemoteDisconnected, - IncompleteRead, - ], - ) - - onyx_rate_limit_error_retry_handler = OnyxRedisSlackRetryHandler( - max_retry_count=max_retry_count, - delay_key=delay_key, - r=r, - ) - custom_retry_handlers: list[RetryHandler] = [ - connection_error_retry_handler, - onyx_rate_limit_error_retry_handler, - ] - - client = OnyxSlackWebClient( - delay_lock=delay_lock, - delay_key=delay_key, - r=r, - token=token, - retry_handlers=custom_retry_handlers, - ) - return client - @property def channels(self) -> list[str] | None: return self._channels @@ -998,43 +950,16 @@ def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None def set_credentials_provider( self, credentials_provider: CredentialsProviderInterface ) -> None: - credentials = credentials_provider.get_credentials() - tenant_id = credentials_provider.get_tenant_id() - if not tenant_id: - raise ValueError("tenant_id cannot be None!") - - bot_token = credentials["slack_bot_token"] - - if self.use_redis: - self.redis = get_redis_client(tenant_id=tenant_id) - self.credential_prefix = SlackConnector.make_credential_prefix( - credentials_provider.get_provider_key() - ) - - self.client = SlackConnector.make_slack_web_client( - self.credential_prefix, bot_token, self.MAX_RETRIES, self.redis - ) - else: - connection_error_retry_handler = ConnectionErrorRetryHandler( - max_retry_count=self.MAX_RETRIES, - interval_calculator=FixedValueRetryIntervalCalculator(), - error_types=[ - URLError, - ConnectionResetError, - RemoteDisconnected, - IncompleteRead, - ], - ) - - self.client = WebClient( - token=bot_token, retry_handlers=[connection_error_retry_handler] - ) - - # use for requests that must return quickly (e.g. realtime flows where user is waiting) - self.fast_client = WebClient( - token=bot_token, timeout=SlackConnector.FAST_TIMEOUT + # The gateway owns client construction (coordinated + fast) and builds + # lazily on first operation, so the credential decrypt happens at first + # remote call rather than here. + self.slack_client = SlackSourceOperations( + credentials_provider=credentials_provider, + connector_specific_config={"use_redis": self.use_redis}, + ) + self.text_cleaner = SlackTextCleaner( + fetch_user_info=self.slack_client.fetch_user_info ) - self.text_cleaner = SlackTextCleaner(client=self.client) self.credentials_provider = credentials_provider with self._workspace_metadata_lock: @@ -1044,10 +969,10 @@ def _ensure_workspace_metadata(self) -> None: """Resolves workspace metadata once, for the indexing paths. Deliberately not resolved in ``set_credentials_provider``: validation - instantiates the connector but never reads this metadata, and - ``self.client`` serializes on a Redis lock shared with running indexing - jobs, so resolving eagerly lets a rate-limited indexing job block - connector creation for the length of its backoff. + instantiates the connector but never reads this metadata, and the + coordinated client serializes on a Redis lock shared with running + indexing jobs, so resolving eagerly lets a rate-limited indexing job + block connector creation for the length of its backoff. ``workspace_url`` and the ``grid_*`` properties are plain accessors, so every entry point that reads them must call this first. @@ -1056,11 +981,15 @@ def _ensure_workspace_metadata(self) -> None: return with self._workspace_metadata_lock: - if self._workspace_metadata is not None or self.client is None: + if self._workspace_metadata is not None or self.slack_client is None: return - self._workspace_metadata = self._resolve_workspace_metadata(self.client) + self._workspace_metadata = self._resolve_workspace_metadata( + self.slack_client + ) - def _resolve_workspace_metadata(self, client: WebClient) -> _WorkspaceMetadata: + def _resolve_workspace_metadata( + self, slack_client: SlackSourceOperations + ) -> _WorkspaceMetadata: """Fails closed: nothing is cached and the index attempt surfaces the error. Grid public-channel ACLs derive from ``team_id_to_user_emails``, and @@ -1068,16 +997,18 @@ def _resolve_workspace_metadata(self, client: WebClient) -> _WorkspaceMetadata: is empty. Swallowing a failure here would silently widen workspace-scoped permissions, so only cosmetic lookups may degrade. """ - auth_response = client.auth_test() + auth_response = slack_client.check_auth() - url = auth_response.get("url") - if not auth_response.get("enterprise_id"): + url = auth_response.url + if not auth_response.enterprise_id: return _WorkspaceMetadata(url=url) - team_ids = list_grid_team_ids(client) - team_id_to_url = self._fetch_team_urls(client, team_ids) if team_ids else {} + team_ids = list_grid_team_ids(slack_client) + team_id_to_url = ( + self._fetch_team_urls(slack_client, team_ids) if team_ids else {} + ) team_id_to_user_emails = ( - fetch_team_user_emails(client, team_ids) if team_ids else {} + fetch_team_user_emails(slack_client, team_ids) if team_ids else {} ) logger.info( @@ -1095,11 +1026,14 @@ def _resolve_workspace_metadata(self, client: WebClient) -> _WorkspaceMetadata: ) @staticmethod - def _fetch_team_urls(client: WebClient, team_ids: list[str]) -> dict[str, str]: + def _fetch_team_urls( + slack_client: SlackSourceOperations, team_ids: list[str] + ) -> dict[str, str]: team_id_to_url: dict[str, str] = {} with ThreadPoolExecutor(max_workers=min(8, len(team_ids))) as executor: futures = { - executor.submit(fetch_team_url, client, tid): tid for tid in team_ids + executor.submit(fetch_team_url, slack_client, tid): tid + for tid in team_ids } for future in as_completed(futures): tid = futures[future] @@ -1119,13 +1053,13 @@ def retrieve_all_slim_docs_perm_sync( end: SecondsSinceUnixEpoch | None = None, callback: IndexingHeartbeatInterface | None = None, ) -> GenerateSlimDocumentOutput: - if self.client is None: + if self.slack_client is None: raise ConnectorMissingCredentialError("Slack") self._ensure_workspace_metadata() return _get_all_doc_ids( - client=self.client, + slack_client=self.slack_client, channels_to_include=self.channels, include_regex_enabled=self.channel_regex_enabled, channels_to_exclude=self.exclude_channels, @@ -1161,7 +1095,7 @@ def _load_from_checkpoint( """ num_channels_remaining = 0 - if self.client is None or self.text_cleaner is None: + if self.slack_client is None or self.text_cleaner is None: raise ConnectorMissingCredentialError("Slack") self._ensure_workspace_metadata() @@ -1174,10 +1108,10 @@ def _load_from_checkpoint( grid_team_ids = self.grid_team_ids if grid_team_ids: raw_channels = get_channels_across_teams( - client=self.client, team_ids=grid_team_ids + slack_client=self.slack_client, team_ids=grid_team_ids ) else: - raw_channels = get_channels(self.client) + raw_channels = get_channels(self.slack_client) filtered_channels = filter_channels( raw_channels, self.channels, @@ -1201,7 +1135,7 @@ def _load_from_checkpoint( # checkpoint.current_channel is guaranteed to be non-None here since we just assigned it assert checkpoint.current_channel is not None channel_access = get_channel_access( - client=self.client, + slack_client=self.slack_client, channel=checkpoint.current_channel, user_cache=self.user_cache, team_id_to_user_emails=(self.grid_team_id_to_user_emails), @@ -1262,7 +1196,7 @@ def _load_from_checkpoint( ) message_batch, has_more_in_channel = _get_messages( - channel, self.client, oldest, latest + channel, self.slack_client, oldest, latest ) logger.info( @@ -1293,7 +1227,7 @@ def _load_from_checkpoint( current_context.run, _process_message, message=message, - client=self.client, + slack_client=self.slack_client, channel=channel, slack_cleaner=self.text_cleaner, user_cache=self.user_cache, @@ -1396,11 +1330,11 @@ def _load_from_checkpoint( ) if new_channel_id: - new_channel = _get_channel_by_id(self.client, new_channel_id) + new_channel = _get_channel_by_id(self.slack_client, new_channel_id) checkpoint.current_channel = new_channel if include_permissions: channel_access = get_channel_access( - client=self.client, + slack_client=self.slack_client, channel=new_channel, user_cache=self.user_cache, team_id_to_user_emails=(self.grid_team_id_to_user_emails), @@ -1472,24 +1406,29 @@ def validate_connector_settings(self) -> None: if self.exclude_channel_regex_enabled: _validate_channel_regexes(self.exclude_channels, "excluded channel") - if self.fast_client is None: + if self.slack_client is None: raise ConnectorMissingCredentialError("Slack credentials not loaded.") try: - # 1) Validate connection to workspace - auth_response = self.fast_client.auth_test() - if not auth_response.get("ok", False): - error_msg = auth_response.get( - "error", "Unknown error from Slack auth_test" - ) + # 1) Validate connection to workspace. ``fast=True``: validation is + # a synchronous user-facing path, so it must not serialize on the + # coordinated client's shared rate-limit lock. + auth_response = self.slack_client.check_auth(fast=True) + if not auth_response.ok: + error_msg = auth_response.error or "Unknown error from Slack auth_test" raise ConnectorValidationError(f"Failed Slack auth_test: {error_msg}") # 2) Minimal test to confirm listing channels works - test_resp = self.fast_client.conversations_list( - limit=1, types=["public_channel"] + test_resp = next( + self.slack_client.list_channels( + variant=SlackChannelVariant.PUBLIC, + channel_types=["public_channel"], + limit=1, + fast=True, + ) ) - if not test_resp.get("ok", False): - error_msg = test_resp.get("error", "Unknown error from Slack") + if not test_resp.ok: + error_msg = test_resp.error or "Unknown error from Slack" if error_msg == "invalid_auth": raise ConnectorValidationError( f"Invalid Slack bot token ({error_msg})." @@ -1504,11 +1443,15 @@ def validate_connector_settings(self) -> None: # 3) Grid: verify team:read, and the users scopes that public-channel # ACLs depend on, so a missing scope fails here instead of at index time. - if auth_response.get("enterprise_id"): - teams_response = self.fast_client.auth_teams_list(limit=1) - teams = teams_response.get("teams", []) + if auth_response.enterprise_id: + teams_response = next(self.slack_client.list_teams(limit=1, fast=True)) + teams = teams_response.teams if teams: - self.fast_client.users_list(team_id=teams[0]["id"], limit=1) + next( + self.slack_client.list_users( + team_id=teams[0]["id"], limit=1, fast=True + ) + ) # 4) If channels are specified and regex is not enabled, verify each is accessible # NOTE: removed this for now since it may be too slow for large workspaces which may @@ -1516,7 +1459,7 @@ def validate_connector_settings(self) -> None: # if self.channels and not self.channel_regex_enabled: # accessible_channels = get_channels( - # client=self.fast_client, + # slack_client=self.slack_client, # exclude_archived=True, # get_public=True, # get_private=True, diff --git a/backend/onyx/connectors/slack/onyx_retry_handler.py b/backend/onyx/connectors/slack/onyx_retry_handler.py deleted file mode 100644 index db59c2bec67..00000000000 --- a/backend/onyx/connectors/slack/onyx_retry_handler.py +++ /dev/null @@ -1,144 +0,0 @@ -import random -from typing import Optional - -from slack_sdk.http_retry.handler import RetryHandler -from slack_sdk.http_retry.request import HttpRequest -from slack_sdk.http_retry.response import HttpResponse -from slack_sdk.http_retry.state import RetryState - -from onyx.redis.tenant_redis_client import TenantRedisClient -from onyx.utils.logger import setup_logger -from onyx.utils.retry_after import parse_retry_after_seconds - -logger = setup_logger() - - -class OnyxRedisSlackRetryHandler(RetryHandler): - """ - This class uses Redis to share a rate limit among multiple threads. - - As currently implemented, this code is already surrounded by a lock in Redis - via an override of _perform_urllib_http_request in OnyxSlackWebClient. - - This just sets the desired retry delay with TTL in redis. In conjunction with - a custom subclass of the client, the value is read and obeyed prior to an API call - and also serialized. - - Another way to do this is just to do exponential backoff. Might be easier? - - Adapted from slack's RateLimitErrorRetryHandler. - """ - - """RetryHandler that does retries for rate limited errors.""" - - def __init__( - self, - max_retry_count: int, - delay_key: str, - r: TenantRedisClient, - ): - """ - delay_lock: the redis key to use with RedisLock (to synchronize access to delay_key) - delay_key: the redis key containing a shared TTL - """ - super().__init__(max_retry_count=max_retry_count) - self._redis: TenantRedisClient = r - self._delay_key = delay_key - - def _can_retry( - self, - *, - state: RetryState, # noqa: ARG002 - request: HttpRequest, # noqa: ARG002 - response: Optional[HttpResponse] = None, - error: Optional[Exception] = None, # noqa: ARG002 - ) -> bool: - return response is not None and response.status_code == 429 - - def prepare_for_next_attempt( - self, - *, - state: RetryState, - request: HttpRequest, # noqa: ARG002 - response: Optional[HttpResponse] = None, - error: Optional[Exception] = None, - ) -> None: - """As initially designed by the SDK authors, this function is responsible for - the wait to retry ... aka we actually sleep in this function. - - This doesn't work well with multiple clients because every thread is unaware - of the current retry value until it actually calls the endpoint. - - We're combining this with an actual subclass of the slack web client so - that the delay is used BEFORE calling an API endpoint. The subclassed client - has already taken the lock in redis when this method is called. - """ - ttl_ms: int | None = None - - retry_after_value: str | None = None - retry_after_header_name: Optional[str] = None - duration_s: float = 1.0 # seconds - - if response is None: - # NOTE(rkuo): this logic comes from RateLimitErrorRetryHandler. - # This reads oddly, as if the caller itself could raise the exception. - # We don't have the luxury of changing this. - if error: - raise error - - return - - state.next_attempt_requested = True # this signals the caller to retry - - # calculate wait duration based on retry-after + some jitter - for k in response.headers.keys(): - if k.lower() == "retry-after": - retry_after_header_name = k - break - - try: - if retry_after_header_name is None: - # This situation usually does not arise. Just in case. - raise ValueError( - "OnyxRedisSlackRetryHandler.prepare_for_next_attempt: retry-after header name is None" - ) - - retry_after_header_value = response.headers.get(retry_after_header_name) - if not retry_after_header_value: - raise ValueError( - "OnyxRedisSlackRetryHandler.prepare_for_next_attempt: retry-after header value is None" - ) - - # Handle case where header value might be a list - retry_after_value = ( - retry_after_header_value[0] - if isinstance(retry_after_header_value, list) - else retry_after_header_value - ) - - parsed_retry_after = parse_retry_after_seconds(retry_after_value) - if parsed_retry_after is None: - raise ValueError( - "OnyxRedisSlackRetryHandler.prepare_for_next_attempt: " - "could not parse retry-after value" - ) - jitter = parsed_retry_after * 0.25 * random.random() - duration_s = parsed_retry_after + jitter - except ValueError: - duration_s += random.random() - - # Read and extend the ttl - ttl_ms = self._redis.pttl(self._delay_key) - if ttl_ms < 0: # negative values are error status codes ... see docs - ttl_ms = 0 - ttl_ms_new = ttl_ms + int(duration_s * 1000.0) - self._redis.set(self._delay_key, "1", px=ttl_ms_new) - - logger.warning( - "OnyxRedisSlackRetryHandler.prepare_for_next_attempt setting delay: current_attempt=%s retry-after=%s ttl_ms_new=%r", - state.current_attempt, - retry_after_value, - ttl_ms_new, - ) - - state.increment_current_attempt() diff --git a/backend/onyx/connectors/slack/onyx_slack_web_client.py b/backend/onyx/connectors/slack/onyx_slack_web_client.py deleted file mode 100644 index a575472ec61..00000000000 --- a/backend/onyx/connectors/slack/onyx_slack_web_client.py +++ /dev/null @@ -1,121 +0,0 @@ -import threading -import time -from typing import Any, Dict -from urllib.request import Request - -from redis.lock import Lock as RedisLock -from slack_sdk import WebClient - -from onyx.connectors.slack.utils import ( - ONYX_SLACK_LOCK_BLOCKING_TIMEOUT, - ONYX_SLACK_LOCK_TOTAL_BLOCKING_TIMEOUT, - ONYX_SLACK_LOCK_TTL, -) -from onyx.redis.tenant_redis_client import TenantRedisClient -from onyx.utils.logger import setup_logger - -logger = setup_logger() - - -class OnyxSlackWebClient(WebClient): - """Use in combination with the Onyx Retry Handler. - - This client wrapper enforces a proper retry delay through redis BEFORE the api call - so that multiple clients can synchronize and rate limit properly. - - The retry handler writes the correct delay value to redis so that it is can be used - by this wrapper. - - """ - - def __init__( - self, - delay_lock: str, - delay_key: str, - r: TenantRedisClient, - *args: Any, - **kwargs: Any, - ) -> None: - super().__init__(*args, **kwargs) - self._delay_key = delay_key - self._delay_lock = delay_lock - self._redis: TenantRedisClient = r - self.num_requests: int = 0 - self._lock = threading.Lock() - - def _perform_urllib_http_request( - self, *, url: str, args: Dict[str, Dict[str, Any]] - ) -> Dict[str, Any]: - """By locking around the base class method, we ensure that both the delay from - Redis and parsing/writing of retry values to Redis are handled properly in - one place""" - # lock and extend the ttl - lock: RedisLock = self._redis.lock( - self._delay_lock, - timeout=ONYX_SLACK_LOCK_TTL, - ) - - # try to acquire the lock - start = time.monotonic() - while True: - acquired = lock.acquire(blocking_timeout=ONYX_SLACK_LOCK_BLOCKING_TIMEOUT) - if acquired: - break - - # if we couldn't acquire the lock but it exists, there's at least some activity - # so keep trying... - if self._redis.exists(self._delay_lock): - continue - - if time.monotonic() - start > ONYX_SLACK_LOCK_TOTAL_BLOCKING_TIMEOUT: - raise RuntimeError( - f"OnyxSlackWebClient._perform_urllib_http_request - " - f"timed out waiting for lock: {ONYX_SLACK_LOCK_TOTAL_BLOCKING_TIMEOUT=}" - ) - - try: - result = super()._perform_urllib_http_request(url=url, args=args) - finally: - if lock.owned(): - lock.release() - else: - logger.warning( - "OnyxSlackWebClient._perform_urllib_http_request lock not owned on release" - ) - - time.monotonic() - start - # logger.info( - # f"OnyxSlackWebClient._perform_urllib_http_request: Releasing lock: {elapsed=}" - # ) - - return result - - def _perform_urllib_http_request_internal( - self, - url: str, - req: Request, - ) -> Dict[str, Any]: - """Overrides the internal method which is mostly the direct call to - urllib/urlopen ... so this is a good place to perform our delay.""" - - # read and execute the delay - delay_ms = self._redis.pttl(self._delay_key) - if delay_ms < 0: # negative values are error status codes ... see docs - delay_ms = 0 - - if delay_ms > 0: - logger.warning( - "OnyxSlackWebClient._perform_urllib_http_request_internal delay: delay_ms=%r self.num_requests=%r", - delay_ms, - self.num_requests, - ) - - time.sleep(delay_ms / 1000.0) - - result = super()._perform_urllib_http_request_internal(url, req) - - with self._lock: - self.num_requests += 1 - - # the delay key should have naturally expired by this point - return result diff --git a/backend/onyx/connectors/slack/source_operations.py b/backend/onyx/connectors/slack/source_operations.py new file mode 100644 index 00000000000..4dd0524473d --- /dev/null +++ b/backend/onyx/connectors/slack/source_operations.py @@ -0,0 +1,823 @@ +"""Slack source-operations gateway: every Slack API call lives here. + +This is the single file in ``onyx/connectors/slack/`` and +``ee/onyx/external_permissions/slack/`` allowed to import ``slack_sdk`` (the +import-fence test enforces it). It owns client construction -- one +redis-coordinated path shared by indexing, perm-sync, and capability checks -- +and exposes each Slack API method as a stamped operation returning its payload +validated into a plain-data model. + +Transport internals (``OnyxSlackWebClient``, ``OnyxRedisSlackRetryHandler``) +were absorbed from their former sibling modules: they subclass slack_sdk types, +so the fence requires them to live here. +""" + +import random +import threading +import time +from collections.abc import Callable, Generator +from enum import Enum +from http.client import IncompleteRead, RemoteDisconnected +from typing import Any, Dict, Optional, TypeVar, cast +from urllib.error import URLError +from urllib.request import Request + +from pydantic import BaseModel, ConfigDict, Field +from redis.lock import Lock as RedisLock +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError as SlackApiError # Re-export, see below. +from slack_sdk.http_retry import ConnectionErrorRetryHandler, RetryHandler +from slack_sdk.http_retry.builtin_interval_calculators import ( + FixedValueRetryIntervalCalculator, +) +from slack_sdk.http_retry.handler import RetryHandler as BaseRetryHandler +from slack_sdk.http_retry.request import HttpRequest +from slack_sdk.http_retry.response import HttpResponse +from slack_sdk.http_retry.state import RetryState +from slack_sdk.web import SlackResponse + +from onyx.configs.constants import DocumentSource +from onyx.connectors.capabilities import CredentialCapability +from onyx.connectors.source_operations import ( + OperationConsumes, + SourceOperations, + source_operation, +) +from onyx.redis.redis_pool import get_redis_client +from onyx.redis.tenant_redis_client import TenantRedisClient +from onyx.utils.logger import setup_logger +from onyx.utils.retry_after import parse_retry_after_seconds + +# ``SlackApiError`` is re-exported as part of the operation contract: callers +# branch on Slack error slugs (``is_archived``, ``missing_scope``, ...) and the +# type carries response data, not a live client, so it does not undermine the +# gateway boundary. Import it from here, never from ``slack_sdk``. + +logger = setup_logger() + +# Messages / items requested per page for paginated Slack calls. +_SLACK_LIMIT = 900 + +# Used to serialize access to the retry TTL. +ONYX_SLACK_LOCK_TTL = 1800 # How long the lock is allowed to idle before it expires. +ONYX_SLACK_LOCK_BLOCKING_TIMEOUT = 60 # How long to wait for the lock per attempt. +ONYX_SLACK_LOCK_TOTAL_BLOCKING_TIMEOUT = 3600 # How long to wait for the lock total. + +_MAX_RETRIES = 7 # Arbitrarily selected. + +# Timeout for the uncoordinated client behind ``fast=True`` operations. +_FAST_TIMEOUT = 1 + +_TEMPORARILY_UNTESTED = "Not yet tested: checks land in the Slack capability checks PR." + + +class SlackChannelVariant(str, Enum): + """Permission class of a channel-scoped operation call. + + Reading private channels needs the ``groups:*`` scope family where public + channels need ``channels:*``; the variant classifies each call so coverage + is counted per permission class. + """ + + PUBLIC = "public" + PRIVATE = "private" + + +class OnyxRedisSlackRetryHandler(BaseRetryHandler): + """This class uses Redis to share a rate limit among multiple threads. + + As currently implemented, this code is already surrounded by a lock in Redis + via an override of _perform_urllib_http_request in OnyxSlackWebClient. + + This just sets the desired retry delay with TTL in redis. In conjunction + with a custom subclass of the client, the value is read and obeyed prior to + an API call and also serialized. + + Another way to do this is just to do exponential backoff. Might be easier? + + Adapted from slack's RateLimitErrorRetryHandler. + """ + + """RetryHandler that does retries for rate limited errors.""" + + def __init__( + self, + max_retry_count: int, + delay_key: str, + r: TenantRedisClient, + ): + """ + delay_lock: the redis key to use with RedisLock (to synchronize access + to delay_key) + delay_key: the redis key containing a shared TTL + """ + super().__init__(max_retry_count=max_retry_count) + self._redis: TenantRedisClient = r + self._delay_key = delay_key + + def _can_retry( + self, + *, + state: RetryState, # noqa: ARG002 + request: HttpRequest, # noqa: ARG002 + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, # noqa: ARG002 + ) -> bool: + return response is not None and response.status_code == 429 + + def prepare_for_next_attempt( + self, + *, + state: RetryState, + request: HttpRequest, # noqa: ARG002 + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> None: + """ + As initially designed by the SDK authors, this function is responsible + for the wait to retry ... aka we actually sleep in this function. + + This doesn't work well with multiple clients because every thread is + unaware of the current retry value until it actually calls the endpoint. + + We're combining this with an actual subclass of the slack web client so + that the delay is used BEFORE calling an API endpoint. The subclassed + client has already taken the lock in redis when this method is called. + """ + ttl_ms: int | None = None + + retry_after_value: str | None = None + retry_after_header_name: Optional[str] = None + duration_s: float = 1.0 # seconds + + if response is None: + # NOTE(rkuo): this logic comes from RateLimitErrorRetryHandler. + # This reads oddly, as if the caller itself could raise the + # exception. We don't have the luxury of changing this. + if error: + raise error + + return + + state.next_attempt_requested = True # this signals the caller to retry + + # Calculate wait duration based on retry-after + some jitter. + for k in response.headers.keys(): + if k.lower() == "retry-after": + retry_after_header_name = k + break + + try: + if retry_after_header_name is None: + # This situation usually does not arise. Just in case. + raise ValueError( + "OnyxRedisSlackRetryHandler.prepare_for_next_attempt: retry-after header name is None" + ) + + retry_after_header_value = response.headers.get(retry_after_header_name) + if not retry_after_header_value: + raise ValueError( + "OnyxRedisSlackRetryHandler.prepare_for_next_attempt: retry-after header value is None" + ) + + # Handle case where header value might be a list. + retry_after_value = ( + retry_after_header_value[0] + if isinstance(retry_after_header_value, list) + else retry_after_header_value + ) + + parsed_retry_after = parse_retry_after_seconds(retry_after_value) + if parsed_retry_after is None: + raise ValueError( + "OnyxRedisSlackRetryHandler.prepare_for_next_attempt: " + "could not parse retry-after value" + ) + jitter = parsed_retry_after * 0.25 * random.random() + duration_s = parsed_retry_after + jitter + except ValueError: + duration_s += random.random() + + # Read and extend the ttl. + ttl_ms = self._redis.pttl(self._delay_key) + if ttl_ms < 0: # negative values are error status codes ... see docs + ttl_ms = 0 + ttl_ms_new = ttl_ms + int(duration_s * 1000.0) + self._redis.set(self._delay_key, "1", px=ttl_ms_new) + + logger.warning( + "OnyxRedisSlackRetryHandler.prepare_for_next_attempt setting delay: current_attempt=%s retry-after=%s ttl_ms_new=%r", + state.current_attempt, + retry_after_value, + ttl_ms_new, + ) + + state.increment_current_attempt() + + +class OnyxSlackWebClient(WebClient): + """Use in combination with the Onyx Retry Handler. + + This client wrapper enforces a proper retry delay through redis BEFORE the + api call so that multiple clients can synchronize and rate limit properly. + + The retry handler writes the correct delay value to redis so that it is can + be used by this wrapper. + """ + + def __init__( + self, + delay_lock: str, + delay_key: str, + r: TenantRedisClient, + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self._delay_key = delay_key + self._delay_lock = delay_lock + self._redis: TenantRedisClient = r + self.num_requests: int = 0 + self._lock = threading.Lock() + + def _perform_urllib_http_request( + self, *, url: str, args: Dict[str, Dict[str, Any]] + ) -> Dict[str, Any]: + """ + By locking around the base class method, we ensure that both the delay + from Redis and parsing/writing of retry values to Redis are handled + properly in one place. + """ + # Lock and extend the ttl. + lock: RedisLock = self._redis.lock( + self._delay_lock, + timeout=ONYX_SLACK_LOCK_TTL, + ) + + # Try to acquire the lock. + start = time.monotonic() + while True: + acquired = lock.acquire(blocking_timeout=ONYX_SLACK_LOCK_BLOCKING_TIMEOUT) + if acquired: + break + + # If we couldn't acquire the lock but it exists, there's at least + # some activity so keep trying... + if self._redis.exists(self._delay_lock): + continue + + if time.monotonic() - start > ONYX_SLACK_LOCK_TOTAL_BLOCKING_TIMEOUT: + raise RuntimeError( + f"OnyxSlackWebClient._perform_urllib_http_request - " + f"timed out waiting for lock: {ONYX_SLACK_LOCK_TOTAL_BLOCKING_TIMEOUT=}" + ) + + try: + result = super()._perform_urllib_http_request(url=url, args=args) + finally: + if lock.owned(): + lock.release() + else: + logger.warning( + "OnyxSlackWebClient._perform_urllib_http_request lock not owned on release" + ) + + return result + + def _perform_urllib_http_request_internal( + self, + url: str, + req: Request, + ) -> Dict[str, Any]: + """ + Overrides the internal method which is mostly the direct call to + urllib/urlopen ... so this is a good place to perform our delay. + """ + + # Read and execute the delay. + delay_ms = self._redis.pttl(self._delay_key) + if delay_ms < 0: # Negative values are error status codes ... see docs. + delay_ms = 0 + + if delay_ms > 0: + logger.warning( + "OnyxSlackWebClient._perform_urllib_http_request_internal delay: delay_ms=%r self.num_requests=%r", + delay_ms, + self.num_requests, + ) + + time.sleep(delay_ms / 1000.0) + + result = super()._perform_urllib_http_request_internal(url, req) + + with self._lock: + self.num_requests += 1 + + # The delay key should have naturally expired by this point. + return result + + +def make_credential_prefix(key: str) -> str: + return f"connector:slack:credential_{key}" + + +def make_delay_lock(prefix: str) -> str: + return f"{prefix}:delay_lock" + + +def make_delay_key(prefix: str) -> str: + return f"{prefix}:delay" + + +def _connection_error_retry_handler( + max_retry_count: int, +) -> ConnectionErrorRetryHandler: + return ConnectionErrorRetryHandler( + max_retry_count=max_retry_count, + interval_calculator=FixedValueRetryIntervalCalculator(), + error_types=[ + URLError, + ConnectionResetError, + RemoteDisconnected, + IncompleteRead, + ], + ) + + +def make_slack_web_client( + prefix: str, token: str, max_retry_count: int, r: TenantRedisClient +) -> WebClient: + """Builds the redis-coordinated client shared by all Slack consumers.""" + delay_lock = make_delay_lock(prefix) + delay_key = make_delay_key(prefix) + + # NOTE: Slack has a built in RateLimitErrorRetryHandler, but it isn't + # designed for concurrent workers. We've extended it with + # OnyxRedisSlackRetryHandler. + onyx_rate_limit_error_retry_handler = OnyxRedisSlackRetryHandler( + max_retry_count=max_retry_count, + delay_key=delay_key, + r=r, + ) + custom_retry_handlers: list[RetryHandler] = [ + _connection_error_retry_handler(max_retry_count), + onyx_rate_limit_error_retry_handler, + ] + + client = OnyxSlackWebClient( + delay_lock=delay_lock, + delay_key=delay_key, + r=r, + token=token, + retry_handlers=custom_retry_handlers, + ) + return client + + +class SlackResponseModel(BaseModel): + """Base for validated Slack payloads. + + Unknown fields are ignored, and each declared field mirrors exactly what + call sites read: required where the pre-gateway code indexed into the + payload, defaulted where it used ``.get`` -- so validation surfaces shape + problems at the operation boundary without inventing new failure modes. Deep + objects (channels, messages, users) stay open dicts: consumers type them via + the existing TypedDicts and read fields the models don't know. + """ + + model_config = ConfigDict(extra="ignore") + + +class SlackResponseMetadata(SlackResponseModel): + """Cursor envelope on paginated responses.""" + + next_cursor: str = "" + + +class SlackPage(SlackResponseModel): + """Envelope shared by all paginated Slack responses.""" + + ok: bool = False + error: str | None = None + response_metadata: SlackResponseMetadata = Field( + default_factory=SlackResponseMetadata + ) + + +class SlackChannelsPage(SlackPage): + channels: list[dict[str, Any]] + + +class SlackTeamsPage(SlackPage): + teams: list[dict[str, Any]] = [] + + +class SlackHistoryPage(SlackPage): + # Tolerant default: the checkpoint indexing path treats missing messages as + # end-of-channel rather than an error. + messages: list[dict[str, Any]] = [] + + +class SlackUsersPage(SlackPage): + members: list[dict[str, Any]] = [] + + +class SlackChannelMembersPage(SlackPage): + # ``conversations.members`` returns member ids, not user objects. + members: list[str] = [] + + +class SlackUsergroupsPage(SlackPage): + usergroups: list[dict[str, Any]] = [] + + +class SlackUsergroupMembersPage(SlackPage): + users: list[str] = [] + + +class SlackAuthTestResponse(SlackResponseModel): + ok: bool = False + error: str | None = None + url: str | None = None + enterprise_id: str | None = None + + +class SlackOkResponse(SlackResponseModel): + ok: bool = False + + +class SlackTeamInfoResponse(SlackResponseModel): + team: dict[str, Any] = {} + + +class SlackChannelInfoResponse(SlackResponseModel): + channel: dict[str, Any] + + +class SlackUserInfoResponse(SlackResponseModel): + ok: bool = False + user: dict[str, Any] = {} + + +_PageT = TypeVar("_PageT", bound=SlackPage) +_ResponseT = TypeVar("_ResponseT", bound=SlackResponseModel) + + +def _paginate( + call: Callable[..., SlackResponse], + page_model: type[_PageT], + limit: int | None = None, + **kwargs: Any, +) -> Generator[_PageT, None, None]: + """Handles cursor pagination; yields each page validated into its model. + + Single-page callers take ``next(...)`` and read the page's + ``response_metadata`` cursor themselves -- pages are lazy, so no second + request fires. + """ + cursor: str | None = None + has_more = True + while has_more: + response = call( + cursor=cursor, + limit=limit if limit is not None else _SLACK_LIMIT, + **kwargs, + ) + response.validate() + page = page_model.model_validate(response.data) + yield page + cursor = page.response_metadata.next_cursor + has_more = bool(cursor) + + +def _validated(response: SlackResponse, model: type[_ResponseT]) -> _ResponseT: + """Validates the payload; operations never return live SDK objects.""" + return model.model_validate(response.data) + + +class SlackSourceOperationsConfig(BaseModel): + """The slice of Slack connector config the gateway consumes. + + Validated from the raw ``connector_specific_config`` dict the gateway is + constructed with. The rest of the connector config (channels, regex flags, + ...) is the connector's business, not the gateway's, hence extra keys are + ignored. + """ + + model_config = ConfigDict(extra="ignore") + + use_redis: bool = True + + +class SlackSourceOperations(SourceOperations): + """All Slack remote interactions, one method per Slack API method. + + Two clients, both owned here: + - The coordinated client (default): redis-synchronized rate limiting shared + across every worker touching this credential, plus connection-error and + rate-limit retry handlers. ``use_redis=False`` in the config + (``SlackSourceOperationsConfig``) downgrades it to a bare client with the + connection-error handler only (dev/test escape hatch, mirrors the old + connector flag). + - The fast client (``fast=True`` operations): bare, ``timeout=1``, no + retries. For synchronous user-facing paths (settings validation) where the + coordinated client may block behind a rate-limited indexing job's backoff + for up to an hour. + + Both are built lazily on first use: construction reads the credential (via + the provider, which audits the decrypt), and credential-time report runs + must not decrypt for gateways whose operations never run. + """ + + source = DocumentSource.SLACK + sdk_modules = ("slack_sdk",) + + _cached_client: WebClient | None = None + _cached_fast_client: WebClient | None = None + # Client construction is idempotent (both builds coordinate through redis), + # so a shared class-level lock is only hygiene against duplicate builds. + _client_build_lock: threading.Lock = threading.Lock() + + def _bot_token(self) -> str: + return cast(str, self.credentials_provider.get_credentials()["slack_bot_token"]) + + def _client(self) -> WebClient: + if self._cached_client is None: + with self._client_build_lock: + if self._cached_client is None: + self._cached_client = self._build_client() + return self._cached_client + + def _fast_client(self) -> WebClient: + if self._cached_fast_client is None: + with self._client_build_lock: + if self._cached_fast_client is None: + self._cached_fast_client = WebClient( + token=self._bot_token(), timeout=_FAST_TIMEOUT + ) + return self._cached_fast_client + + def _config(self) -> SlackSourceOperationsConfig: + return SlackSourceOperationsConfig.model_validate( + self.connector_specific_config or {} + ) + + def _build_client(self) -> WebClient: + token = self._bot_token() + if not self._config().use_redis: + return WebClient( + token=token, + retry_handlers=[_connection_error_retry_handler(_MAX_RETRIES)], + ) + tenant_id = self.credentials_provider.get_tenant_id() + if not tenant_id: + raise ValueError("tenant_id cannot be None!") + prefix = make_credential_prefix(self.credentials_provider.get_provider_key()) + return make_slack_web_client( + prefix, token, _MAX_RETRIES, get_redis_client(tenant_id=tenant_id) + ) + + def _client_for(self, fast: bool) -> WebClient: + return self._fast_client() if fast else self._client() + + @source_operation( + capabilities={ + CredentialCapability.INDEXING, + CredentialCapability.DOC_PERMISSION_SYNC, + }, + consumes=OperationConsumes.CREDENTIAL, + untested=_TEMPORARILY_UNTESTED, + ) + def check_auth(self, *, fast: bool = False) -> SlackAuthTestResponse: + """``auth.test``: token validity, workspace url, Grid enterprise id.""" + return _validated(self._client_for(fast).auth_test(), SlackAuthTestResponse) + + @source_operation( + capabilities={ + CredentialCapability.INDEXING, + CredentialCapability.DOC_PERMISSION_SYNC, + }, + consumes=OperationConsumes.CREDENTIAL, + variants=(SlackChannelVariant.PUBLIC, SlackChannelVariant.PRIVATE), + untested=_TEMPORARILY_UNTESTED, + ) + def list_channels( + self, + *, + variant: SlackChannelVariant, + channel_types: list[str], + exclude_archived: bool | None = None, + team_id: str | None = None, + limit: int | None = None, + fast: bool = False, + ) -> Generator[SlackChannelsPage, None, None]: + """``conversations.list``, paginated. + + The variant names the permission class: a call whose ``channel_types`` + include private channels classifies as ``private`` (it fails without + ``groups:read``), public-only calls as ``public``. + """ + del variant # Classification-only; ``channel_types`` carries the request. + kwargs: dict[str, Any] = {"types": channel_types} + if exclude_archived is not None: + kwargs["exclude_archived"] = exclude_archived + if team_id is not None: + kwargs["team_id"] = team_id + return _paginate( + self._client_for(fast).conversations_list, + SlackChannelsPage, + limit=limit, + **kwargs, + ) + + @source_operation( + capabilities={ + CredentialCapability.INDEXING, + CredentialCapability.DOC_PERMISSION_SYNC, + }, + consumes=OperationConsumes.CREDENTIAL, + untested=_TEMPORARILY_UNTESTED, + ) + def list_teams( + self, *, limit: int | None = None, fast: bool = False + ) -> Generator[SlackTeamsPage, None, None]: + """ + ``auth.teams.list``, paginated: Grid org workspaces (``team:read``). + """ + return _paginate( + self._client_for(fast).auth_teams_list, SlackTeamsPage, limit=limit + ) + + @source_operation( + capabilities={CredentialCapability.INDEXING}, + consumes=OperationConsumes.CREDENTIAL, + untested="Production degrades gracefully: a failed team.info only costs " + "the workspace URL on Grid message links.", + ) + def fetch_team_info(self, *, team_id: str) -> SlackTeamInfoResponse: + """``team.info`` for one Grid workspace.""" + return _validated(self._client().team_info(team=team_id), SlackTeamInfoResponse) + + @source_operation( + capabilities={CredentialCapability.INDEXING}, + consumes=OperationConsumes.CREDENTIAL, + untested="Side effect: joins the bot to the channel; probing it would " + "mutate workspace state.", + ) + def join_channel(self, *, channel_id: str) -> SlackOkResponse: + """``conversations.join``: the bot must be a member to read messages. + + Only works for public channels; private-channel membership requires an + invite, so joining one raises a ``SlackApiError``. + """ + return _validated( + self._client().conversations_join(channel=channel_id), SlackOkResponse + ) + + @source_operation( + capabilities={CredentialCapability.INDEXING}, + consumes=OperationConsumes.CREDENTIAL, + variants=(SlackChannelVariant.PUBLIC, SlackChannelVariant.PRIVATE), + untested=_TEMPORARILY_UNTESTED, + ) + def fetch_channel_history( + self, + *, + variant: SlackChannelVariant, + channel_id: str, + oldest: str | None = None, + latest: str | None = None, + limit: int | None = None, + ) -> Generator[SlackHistoryPage, None, None]: + """``conversations.history``, paginated. + + The variant is the channel's privacy (``channels:history`` vs + ``groups:history``), from ``channel["is_private"]`` at the call site. + """ + del variant # Classification-only; the channel id carries the request. + return _paginate( + self._client().conversations_history, + SlackHistoryPage, + limit=limit, + channel=channel_id, + oldest=oldest, + latest=latest, + ) + + @source_operation( + capabilities={CredentialCapability.INDEXING}, + consumes=OperationConsumes.CREDENTIAL, + variants=(SlackChannelVariant.PUBLIC, SlackChannelVariant.PRIVATE), + untested=_TEMPORARILY_UNTESTED, + ) + def fetch_thread_replies( + self, *, variant: SlackChannelVariant, channel_id: str, thread_ts: str + ) -> Generator[SlackHistoryPage, None, None]: + """``conversations.replies``, paginated. Variant as in history.""" + del variant # Classification-only; the channel id carries the request. + return _paginate( + self._client().conversations_replies, + SlackHistoryPage, + channel=channel_id, + ts=thread_ts, + ) + + @source_operation( + capabilities={CredentialCapability.INDEXING}, + consumes=OperationConsumes.CREDENTIAL, + untested=_TEMPORARILY_UNTESTED, + ) + def fetch_channel_info(self, *, channel_id: str) -> SlackChannelInfoResponse: + """ + ``conversations.info``. No variants: the call exists to discover the + channel (including its privacy), so it cannot classify itself. + """ + return _validated( + self._client().conversations_info(channel=channel_id), + SlackChannelInfoResponse, + ) + + @source_operation( + capabilities={ + CredentialCapability.INDEXING, + CredentialCapability.DOC_PERMISSION_SYNC, + CredentialCapability.EXTERNAL_GROUP_SYNC, + }, + consumes=OperationConsumes.CREDENTIAL, + untested=_TEMPORARILY_UNTESTED, + ) + def fetch_user_info(self, user_id: str) -> SlackUserInfoResponse: + """ + ``users.info``. Positional ``user_id`` on purpose: the bound method + doubles as the ``FetchUserInfo`` callable that user-resolving helpers + (shared with onyxbot) take injected. + """ + return _validated( + self._client().users_info(user=user_id), SlackUserInfoResponse + ) + + @source_operation( + capabilities={ + CredentialCapability.INDEXING, + CredentialCapability.DOC_PERMISSION_SYNC, + }, + consumes=OperationConsumes.CREDENTIAL, + untested=_TEMPORARILY_UNTESTED, + ) + def list_users( + self, + *, + team_id: str | None = None, + limit: int | None = None, + fast: bool = False, + ) -> Generator[SlackUsersPage, None, None]: + """ + ``users.list``, paginated; ``team_id`` scopes to one Grid workspace. + """ + kwargs: dict[str, Any] = {} + if team_id is not None: + kwargs["team_id"] = team_id + return _paginate( + self._client_for(fast).users_list, SlackUsersPage, limit=limit, **kwargs + ) + + @source_operation( + capabilities={CredentialCapability.DOC_PERMISSION_SYNC}, + consumes=OperationConsumes.CREDENTIAL, + untested=_TEMPORARILY_UNTESTED, + ) + def list_channel_members( + self, *, channel_id: str + ) -> Generator[SlackChannelMembersPage, None, None]: + """``conversations.members``, paginated.""" + return _paginate( + self._client().conversations_members, + SlackChannelMembersPage, + channel=channel_id, + ) + + @source_operation( + capabilities={CredentialCapability.EXTERNAL_GROUP_SYNC}, + consumes=OperationConsumes.CREDENTIAL, + untested="Dormant by design: Slack group sync is unused (channel access " + "resolves usergroups to individual users).", + ) + def list_usergroups(self) -> Generator[SlackUsergroupsPage, None, None]: + """``usergroups.list``, paginated.""" + return _paginate(self._client().usergroups_list, SlackUsergroupsPage) + + @source_operation( + capabilities={CredentialCapability.EXTERNAL_GROUP_SYNC}, + consumes=OperationConsumes.CREDENTIAL, + untested="Dormant by design: Slack group sync is unused (channel access " + "resolves usergroups to individual users).", + ) + def list_usergroup_members( + self, *, usergroup_id: str + ) -> Generator[SlackUsergroupMembersPage, None, None]: + """``usergroups.users.list``, paginated.""" + return _paginate( + self._client().usergroups_users_list, + SlackUsergroupMembersPage, + usergroup=usergroup_id, + ) diff --git a/backend/onyx/connectors/slack/utils.py b/backend/onyx/connectors/slack/utils.py index 4119fee1110..ef533c1a5aa 100644 --- a/backend/onyx/connectors/slack/utils.py +++ b/backend/onyx/connectors/slack/utils.py @@ -1,23 +1,22 @@ import re -from collections.abc import Callable, Generator -from functools import lru_cache, wraps -from typing import Any, cast - -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError -from slack_sdk.web import SlackResponse +from collections.abc import Callable +from functools import lru_cache from onyx.connectors.models import BasicExpertInfo from onyx.connectors.slack.models import MessageType +from onyx.connectors.slack.source_operations import ( + SlackApiError, + SlackSourceOperations, + SlackUserInfoResponse, +) from onyx.utils.logger import setup_logger -from onyx.utils.retry_wrapper import retry_builder logger = setup_logger() -# retry after 0.1, 1.2, 3.4, 7.8, 16.6, 34.2 seconds -basic_retry_wrapper = retry_builder(tries=7) -# number of messages we request per page when fetching paginated slack messages -_SLACK_LIMIT = 900 +# Resolves one Slack user id to a validated ``users.info`` payload. The +# gateway's ``fetch_user_info`` operation satisfies this; onyxbot (which keeps +# its own bot-app WebClient and no gateway) passes an adapter over that client. +FetchUserInfo = Callable[[str], SlackUserInfoResponse] # Skips an @ preceded by a word character, where a zero-width space would # split an address rather than defang a mention @@ -39,21 +38,21 @@ def _defang_mentions(text: str) -> str: return _MENTION_AT_PATTERN.sub("@​", text) -# used to serialize access to the retry TTL -ONYX_SLACK_LOCK_TTL = 1800 # how long the lock is allowed to idle before it expires -ONYX_SLACK_LOCK_BLOCKING_TIMEOUT = 60 # how long to wait for the lock per wait attempt -ONYX_SLACK_LOCK_TOTAL_BLOCKING_TIMEOUT = 3600 # how long to wait for the lock in total - - @lru_cache() -def get_base_url(token: str) -> str: - """Retrieve and cache the base URL of the Slack workspace based on the client token.""" - client = WebClient(token=token) - return client.auth_test()["url"] +def get_base_url(slack_client: SlackSourceOperations) -> str: + """Retrieve and cache the base URL of the Slack workspace for a gateway. + + Cached per gateway instance (one credential each), replacing the old + per-token cache. + """ + url = slack_client.check_auth().url + if not url: + raise RuntimeError("auth.test response contained no workspace url.") + return url def fetch_team_user_emails( - slack_client: WebClient, + slack_client: SlackSourceOperations, team_ids: list[str], ) -> dict[str, set[str]]: """Per-workspace user email sets. Used to scope public-channel access on @@ -62,10 +61,8 @@ def fetch_team_user_emails( result: dict[str, set[str]] = {} for tid in team_ids: emails: set[str] = set() - for user_info in make_paginated_slack_api_call( - slack_client.users_list, team_id=tid - ): - for user in user_info.get("members", []): + for user_info in slack_client.list_users(team_id=tid): + for user in user_info.members: email = user.get("profile", {}).get("email") if email: emails.add(email) @@ -75,7 +72,7 @@ def fetch_team_user_emails( def get_message_link( event: MessageType, - client: WebClient, + slack_client: SlackSourceOperations, channel_id: str, team_id: str | None = None, team_id_to_url: dict[str, str] | None = None, @@ -88,7 +85,7 @@ def get_message_link( if team_id and team_id_to_url is not None: base_url = team_id_to_url.get(team_id) if not base_url: - base_url = get_base_url(client.token) + base_url = get_base_url(slack_client) link = f"{base_url.rstrip('/')}/archives/{channel_id}/p{message_ts_without_dot}" + ( f"?thread_ts={thread_ts}" if thread_ts else "" @@ -96,110 +93,9 @@ def get_message_link( return link -def make_slack_api_call( - call: Callable[..., SlackResponse], **kwargs: Any -) -> SlackResponse: - return call(**kwargs) - - -def make_paginated_slack_api_call( - call: Callable[..., SlackResponse], **kwargs: Any -) -> Generator[dict[str, Any], None, None]: - return _make_slack_api_call_paginated(call)(**kwargs) - - -def _make_slack_api_call_paginated( - call: Callable[..., SlackResponse], -) -> Callable[..., Generator[dict[str, Any], None, None]]: - """Wraps calls to slack API so that they automatically handle pagination""" - - @wraps(call) - def paginated_call(**kwargs: Any) -> Generator[dict[str, Any], None, None]: - cursor: str | None = None - has_more = True - while has_more: - response = call(cursor=cursor, limit=_SLACK_LIMIT, **kwargs) - yield cast(dict[str, Any], response.validate()) - cursor = cast(dict[str, Any], response.get("response_metadata", {})).get( - "next_cursor", "" - ) - has_more = bool(cursor) - - return paginated_call - - -# NOTE(rkuo): we may not need this any more if the integrated retry handlers work as -# expected. Do we want to keep this around? - -# def make_slack_api_rate_limited( -# call: Callable[..., SlackResponse], max_retries: int = 7 -# ) -> Callable[..., SlackResponse]: -# """Wraps calls to slack API so that they automatically handle rate limiting""" - -# @wraps(call) -# def rate_limited_call(**kwargs: Any) -> SlackResponse: -# last_exception = None - -# for _ in range(max_retries): -# try: -# # Make the API call -# response = call(**kwargs) - -# # Check for errors in the response, will raise `SlackApiError` -# # if anything went wrong -# response.validate() -# return response - -# except SlackApiError as e: -# last_exception = e -# try: -# error = e.response["error"] -# except KeyError: -# error = "unknown error" - -# if error == "ratelimited": -# # Handle rate limiting: get the 'Retry-After' header value and sleep for that duration -# retry_after = int(e.response.headers.get("Retry-After", 1)) -# logger.info( -# f"Slack call rate limited, retrying after {retry_after} seconds. Exception: {e}" -# ) -# time.sleep(retry_after) -# elif error in ["already_reacted", "no_reaction", "internal_error"]: -# # Log internal_error and return the response instead of failing -# logger.warning( -# f"Slack call encountered '{error}', skipping and continuing..." -# ) -# return e.response -# else: -# # Raise the error for non-transient errors -# raise - -# # If the code reaches this point, all retries have been exhausted -# msg = f"Max retries ({max_retries}) exceeded" -# if last_exception: -# raise Exception(msg) from last_exception -# else: -# raise Exception(msg) - -# return rate_limited_call - -# temporarily disabling due to using a different retry approach -# might be permanent if everything works out -# def make_slack_api_call_w_retries( -# call: Callable[..., SlackResponse], **kwargs: Any -# ) -> SlackResponse: -# return basic_retry_wrapper(call)(**kwargs) - - -# def make_paginated_slack_api_call_w_retries( -# call: Callable[..., SlackResponse], **kwargs: Any -# ) -> Generator[dict[str, Any], None, None]: -# return _make_slack_api_call_paginated(basic_retry_wrapper(call))(**kwargs) - - def expert_info_from_slack_id( user_id: str | None, - client: WebClient, + fetch_user_info: FetchUserInfo, user_cache: dict[str, BasicExpertInfo | None], ) -> BasicExpertInfo | None: if not user_id: @@ -208,13 +104,13 @@ def expert_info_from_slack_id( if user_id in user_cache: return user_cache[user_id] - response = client.users_info(user=user_id) + response = fetch_user_info(user_id) - if not response["ok"]: + if not response.ok: user_cache[user_id] = None return None - user: dict = cast(dict[Any, dict], response.data).get("user", {}) + user = response.user profile = user.get("profile", {}) expert = BasicExpertInfo( @@ -234,18 +130,18 @@ class SlackTextCleaner: Handles caching, so the same request is not made multiple times for the same user ID""" - def __init__(self, client: WebClient) -> None: - self._client = client + def __init__(self, fetch_user_info: FetchUserInfo) -> None: + self._fetch_user_info = fetch_user_info self._id_to_name_map: dict[str, str] = {} def _get_slack_name(self, user_id: str) -> str: if user_id not in self._id_to_name_map: try: - response = self._client.users_info(user=user_id) + response = self._fetch_user_info(user_id) # prefer display name if set, since that is what is shown in Slack self._id_to_name_map[user_id] = ( - response["user"]["profile"]["display_name"] - or response["user"]["profile"]["real_name"] + response.user["profile"]["display_name"] + or response.user["profile"]["real_name"] ) except SlackApiError as e: # Common per-message condition: user was deleted, workspace diff --git a/backend/onyx/file_processing/html_utils.py b/backend/onyx/file_processing/html_utils.py index 57c7d9d106b..fd0f9971e68 100644 --- a/backend/onyx/file_processing/html_utils.py +++ b/backend/onyx/file_processing/html_utils.py @@ -18,6 +18,9 @@ logger = setup_logger() MINTLIFY_UNWANTED = ["sticky", "hidden"] +_ANCHOR_ELEMENT = "a" +_HREF_ATTRIBUTE = "href" +_TABLE_ELEMENT = "table" @dataclass @@ -54,6 +57,20 @@ def format_element_text(element_text: str, link_href: str | None) -> str: return f"[{element_text_no_newlines}]({link_href})" +def _get_ancestor_link_href( + element: bs4.element.PageElement, in_table: bool +) -> str | None: + if in_table: + return None + + link = element.find_parent(_ANCHOR_ELEMENT) + if not link: + return None + + href = link.get(_HREF_ATTRIBUTE) + return href[0] if isinstance(href, list) else href + + def parse_html_with_trafilatura(html_content: str) -> str: """Parse HTML content using trafilatura.""" import trafilatura @@ -84,15 +101,14 @@ def format_document_soup( text = "" list_element_start = False verbatim_output = 0 - in_table = False last_added_newline = False - link_href: str | None = None for e in document.descendants: verbatim_output -= 1 if isinstance(e, bs4.element.NavigableString): if isinstance(e, (bs4.element.Comment, bs4.element.Doctype)): continue + in_table = e.find_parent(_TABLE_ELEMENT) is not None element_text = e.text if in_table: # Tables are represented in natural language with rows separated by newlines @@ -110,7 +126,9 @@ def format_document_soup( content_to_add = ( element_text if verbatim_output > 0 - else format_element_text(element_text, link_href) + else format_element_text( + element_text, _get_ancestor_link_href(e, in_table) + ) ) # Don't join separate elements without any spacing @@ -123,28 +141,16 @@ def format_document_soup( list_element_start = False elif isinstance(e, bs4.element.Tag): - # table is standard HTML element - if e.name == "table": - in_table = True + in_table = e.find_parent(_TABLE_ELEMENT) is not None # tr is for rows - elif e.name == "tr" and in_table: + if e.name == "tr" and in_table: text += "\n" # td for data cell, th for header elif e.name in ["td", "th"] and in_table: text += table_cell_separator - elif e.name == "/table": - in_table = False elif in_table: # don't handle other cases while in table pass - elif e.name == "a": - href_value = e.get("href", None) - # mostly for typing, having multiple hrefs is not valid HTML - link_href = ( - href_value[0] if isinstance(href_value, list) else href_value - ) - elif e.name == "/a": - link_href = None elif e.name in ["p", "div"]: if not list_element_start: text += "\n" diff --git a/backend/onyx/main.py b/backend/onyx/main.py index 7372a299200..2c31326c241 100644 --- a/backend/onyx/main.py +++ b/backend/onyx/main.py @@ -159,7 +159,6 @@ from onyx.server.security.api import admin_router as security_admin_router from onyx.server.settings.api import admin_router as settings_admin_router from onyx.server.settings.api import basic_router as settings_router -from onyx.server.token_rate_limits.api import router as token_rate_limit_settings_router from onyx.server.utils import BasicAuthenticationError from onyx.setup import setup_multitenant_onyx, setup_onyx from onyx.tracing.setup import setup_tracing @@ -590,9 +589,6 @@ def get_application(lifespan_override: Lifespan | None = None) -> FastAPI: include_router_with_global_prefix_prepended( application, opensearch_migration_admin_router ) - include_router_with_global_prefix_prepended( - application, token_rate_limit_settings_router - ) include_router_with_global_prefix_prepended(application, cost_override_router) include_router_with_global_prefix_prepended(application, user_usage_router) include_router_with_global_prefix_prepended(application, admin_usage_router) diff --git a/backend/onyx/onyxbot/slack/handlers/handle_buttons.py b/backend/onyx/onyxbot/slack/handlers/handle_buttons.py index 633908416e6..61cd96ef193 100644 --- a/backend/onyx/onyxbot/slack/handlers/handle_buttons.py +++ b/backend/onyx/onyxbot/slack/handlers/handle_buttons.py @@ -38,6 +38,7 @@ from onyx.onyxbot.slack.models import SlackMessageInfo from onyx.onyxbot.slack.utils import ( TenantSocketModeClient, + bot_user_info_fetcher, build_feedback_id, decompose_action_id, fetch_group_ids_from_names, @@ -129,7 +130,9 @@ def handle_generate_answer_button( message_ts = req.payload["message"]["ts"] thread_ts = req.payload["container"].get("thread_ts", None) user_id = req.payload["user"]["id"] - expert_info = expert_info_from_slack_id(user_id, client.web_client, user_cache={}) + expert_info = expert_info_from_slack_id( + user_id, bot_user_info_fetcher(client.web_client), user_cache={} + ) email = expert_info.email if expert_info else None if not thread_ts: @@ -372,7 +375,7 @@ def handle_slack_feedback( # Get Onyx user from Slack ID expert_info = expert_info_from_slack_id( - user_id_to_post_confirmation, client, user_cache={} + user_id_to_post_confirmation, bot_user_info_fetcher(client), user_cache={} ) email = expert_info.email if expert_info else None diff --git a/backend/onyx/onyxbot/slack/listener.py b/backend/onyx/onyxbot/slack/listener.py index d3ba2fa466e..5d5683a50de 100644 --- a/backend/onyx/onyxbot/slack/listener.py +++ b/backend/onyx/onyxbot/slack/listener.py @@ -80,6 +80,7 @@ from onyx.onyxbot.slack.models import SlackContext, SlackMessageInfo, ThreadMessage from onyx.onyxbot.slack.utils import ( TenantSocketModeClient, + bot_user_info_fetcher, check_message_limit, decompose_action_id, get_channel_name_from_id, @@ -940,7 +941,7 @@ def build_request_details( thread_ts = event.get("thread_ts") sender_id = event.get("user") or None expert_info = expert_info_from_slack_id( - sender_id, client.web_client, user_cache={} + sender_id, bot_user_info_fetcher(client.web_client), user_cache={} ) email = expert_info.email if expert_info else None @@ -1024,7 +1025,7 @@ def build_request_details( msg = req.payload["text"] sender = req.payload["user_id"] expert_info = expert_info_from_slack_id( - sender, client.web_client, user_cache={} + sender, bot_user_info_fetcher(client.web_client), user_cache={} ) email = expert_info.email if expert_info else None diff --git a/backend/onyx/onyxbot/slack/utils.py b/backend/onyx/onyxbot/slack/utils.py index f206ce2fa22..960b51ea2b2 100644 --- a/backend/onyx/onyxbot/slack/utils.py +++ b/backend/onyx/onyxbot/slack/utils.py @@ -24,7 +24,8 @@ ONYX_BOT_RESPONSE_LIMIT_PER_TIME_PERIOD, ONYX_BOT_RESPONSE_LIMIT_TIME_PERIOD_SECONDS, ) -from onyx.connectors.slack.utils import SlackTextCleaner +from onyx.connectors.slack.source_operations import SlackUserInfoResponse +from onyx.connectors.slack.utils import FetchUserInfo, SlackTextCleaner from onyx.db.engine.sql_engine import get_session_with_current_tenant from onyx.db.users import get_user_by_email from onyx.onyxbot.slack.constants import FeedbackVisibility @@ -44,6 +45,16 @@ _ONYX_BOT_COUNT_START_TIME: float = time.time() +def bot_user_info_fetcher(client: WebClient) -> FetchUserInfo: + """ + Adapts a bot WebClient to the ``FetchUserInfo`` contract of the shared slack + helpers (the connector satisfies it via its gateway operation). + """ + return lambda user_id: SlackUserInfoResponse.model_validate( + client.users_info(user=user_id).data + ) + + def get_onyx_bot_auth_ids( tenant_id: str, slack_bot_id: int, web_client: WebClient ) -> tuple[str | None, str | None]: diff --git a/backend/onyx/server/token_rate_limits/api.py b/backend/onyx/server/token_rate_limits/api.py deleted file mode 100644 index 4812b6afe87..00000000000 --- a/backend/onyx/server/token_rate_limits/api.py +++ /dev/null @@ -1,92 +0,0 @@ -from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session - -from onyx.auth.permissions import require_permission -from onyx.configs.constants import PUBLIC_API_TAGS -from onyx.db.engine.sql_engine import get_session -from onyx.db.enums import Permission -from onyx.db.models import User -from onyx.db.token_limit import ( - delete_token_rate_limit, - fetch_all_global_token_rate_limits, - insert_global_token_rate_limit, - update_token_rate_limit, -) -from onyx.server.query_and_chat.token_limit import ( - invalidate_any_rate_limit_exists_cache, -) -from onyx.server.token_rate_limits.models import ( - TokenRateLimitArgs, - TokenRateLimitDisplay, - TokenRateLimitUpdateArgs, -) - -router = APIRouter(prefix="/admin/token-rate-limits", tags=PUBLIC_API_TAGS) - - -""" -Global Token Limit Settings -""" - - -@router.get("/global") -def get_global_token_limit_settings( - _: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)), - db_session: Session = Depends(get_session), -) -> list[TokenRateLimitDisplay]: - return [ - TokenRateLimitDisplay.from_db(token_rate_limit) - for token_rate_limit in fetch_all_global_token_rate_limits(db_session) - ] - - -@router.post("/global") -def create_global_token_limit_settings( - token_limit_settings: TokenRateLimitArgs, - _: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)), - db_session: Session = Depends(get_session), -) -> TokenRateLimitDisplay: - rate_limit_display = TokenRateLimitDisplay.from_db( - insert_global_token_rate_limit(db_session, token_limit_settings) - ) - # clear cache in case this was the first rate limit created - invalidate_any_rate_limit_exists_cache() - return rate_limit_display - - -""" -General Token Limit Settings -""" - - -@router.put("/rate-limit/{token_rate_limit_id}") -def update_token_limit_settings( - token_rate_limit_id: int, - token_limit_settings: TokenRateLimitUpdateArgs, - _: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)), - db_session: Session = Depends(get_session), -) -> TokenRateLimitDisplay: - rate_limit_display = TokenRateLimitDisplay.from_db( - update_token_rate_limit( - db_session=db_session, - token_rate_limit_id=token_rate_limit_id, - token_rate_limit_settings=token_limit_settings, - ) - ) - # enabling/disabling a limit changes whether any rate limit exists - invalidate_any_rate_limit_exists_cache() - return rate_limit_display - - -@router.delete("/rate-limit/{token_rate_limit_id}") -def delete_token_limit_settings( - token_rate_limit_id: int, - _: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)), - db_session: Session = Depends(get_session), -) -> None: - delete_token_rate_limit( - db_session=db_session, - token_rate_limit_id=token_rate_limit_id, - ) - # removing a limit can change whether any rate limit exists - invalidate_any_rate_limit_exists_cache() diff --git a/backend/requirements/default.txt b/backend/requirements/default.txt index 35bcd49550e..4c0bd13d0d4 100644 --- a/backend/requirements/default.txt +++ b/backend/requirements/default.txt @@ -26,72 +26,72 @@ aiohappyeyeballs==2.6.1 \ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 # via aiohttp -aiohttp==3.14.1 \ - --hash=sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983 \ - --hash=sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340 \ - --hash=sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d \ - --hash=sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a \ - --hash=sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4 \ - --hash=sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8 \ - --hash=sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397 \ - --hash=sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8 \ - --hash=sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09 \ - --hash=sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba \ - --hash=sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf \ - --hash=sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5 \ - --hash=sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035 \ - --hash=sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6 \ - --hash=sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35 \ - --hash=sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333 \ - --hash=sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203 \ - --hash=sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1 \ - --hash=sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251 \ - --hash=sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365 \ - --hash=sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94 \ - --hash=sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da \ - --hash=sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe \ - --hash=sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080 \ - --hash=sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397 \ - --hash=sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9 \ - --hash=sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345 \ - --hash=sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3 \ - --hash=sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602 \ - --hash=sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444 \ - --hash=sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15 \ - --hash=sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe \ - --hash=sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2 \ - --hash=sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496 \ - --hash=sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876 \ - --hash=sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448 \ - --hash=sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6 \ - --hash=sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd \ - --hash=sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f \ - --hash=sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c \ - --hash=sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca \ - --hash=sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c \ - --hash=sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa \ - --hash=sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc \ - --hash=sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0 \ - --hash=sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0 \ - --hash=sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844 \ - --hash=sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719 \ - --hash=sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1 \ - --hash=sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178 \ - --hash=sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95 \ - --hash=sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96 \ - --hash=sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1 \ - --hash=sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a \ - --hash=sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588 \ - --hash=sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec \ - --hash=sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004 \ - --hash=sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780 \ - --hash=sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d \ - --hash=sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca \ - --hash=sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665 \ - --hash=sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296 \ - --hash=sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c \ - --hash=sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451 \ - --hash=sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3 +aiohttp==3.14.3 \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 # via # aiobotocore # discord-py @@ -923,9 +923,9 @@ gitdb==4.0.12 \ --hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \ --hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf # via gitpython -gitpython==3.1.57 \ - --hash=sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf \ - --hash=sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488 +gitpython==3.1.58 \ + --hash=sha256:621416df10ef3fd0e19fabf9172ddeed0fa704d353d04f194eec56a625a95b22 \ + --hash=sha256:d331e722577f0fd7fc1f857419b3ecc07af66282b933d2a4d95f84a042fdd50f # via braintrust google-api-core==2.28.1 \ --hash=sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8 \ @@ -1092,9 +1092,9 @@ h11==0.16.0 \ # mitmproxy # uvicorn # wsproto -h2==4.3.0 \ - --hash=sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1 \ - --hash=sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd +h2==4.4.1 \ + --hash=sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6 \ + --hash=sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516 # via # httpx # mitmproxy @@ -1117,9 +1117,9 @@ hf-xet==1.6.0 ; platform_machine == 'AMD64' or platform_machine == 'aarch64' or --hash=sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b \ --hash=sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b # via huggingface-hub -hpack==4.1.0 \ - --hash=sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496 \ - --hash=sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca +hpack==4.2.0 \ + --hash=sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0 \ + --hash=sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986 # via h2 html5lib==1.1 \ --hash=sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d \ @@ -2382,9 +2382,9 @@ pyparsing==3.2.0 \ # via # httplib2 # mitmproxy -pypdf==6.14.2 \ - --hash=sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946 \ - --hash=sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25 +pypdf==6.15.0 \ + --hash=sha256:14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee \ + --hash=sha256:d39c4d955a76409284a905e2d65b40076d77ab76129e0faaeeb6612403ecfc79 # via unstructured-client pypdfium2==5.9.0 \ --hash=sha256:040f5513b808db705d4878f57e2bf0b9dc6e6a0ad8d765c36cf62febf3933b28 \ diff --git a/backend/requirements/dev.txt b/backend/requirements/dev.txt index 357e5804a21..cd1db146b02 100644 --- a/backend/requirements/dev.txt +++ b/backend/requirements/dev.txt @@ -20,72 +20,72 @@ aiohappyeyeballs==2.6.1 \ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 # via aiohttp -aiohttp==3.14.1 \ - --hash=sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983 \ - --hash=sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340 \ - --hash=sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d \ - --hash=sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a \ - --hash=sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4 \ - --hash=sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8 \ - --hash=sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397 \ - --hash=sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8 \ - --hash=sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09 \ - --hash=sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba \ - --hash=sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf \ - --hash=sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5 \ - --hash=sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035 \ - --hash=sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6 \ - --hash=sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35 \ - --hash=sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333 \ - --hash=sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203 \ - --hash=sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1 \ - --hash=sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251 \ - --hash=sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365 \ - --hash=sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94 \ - --hash=sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da \ - --hash=sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe \ - --hash=sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080 \ - --hash=sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397 \ - --hash=sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9 \ - --hash=sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345 \ - --hash=sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3 \ - --hash=sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602 \ - --hash=sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444 \ - --hash=sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15 \ - --hash=sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe \ - --hash=sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2 \ - --hash=sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496 \ - --hash=sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876 \ - --hash=sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448 \ - --hash=sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6 \ - --hash=sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd \ - --hash=sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f \ - --hash=sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c \ - --hash=sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca \ - --hash=sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c \ - --hash=sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa \ - --hash=sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc \ - --hash=sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0 \ - --hash=sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0 \ - --hash=sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844 \ - --hash=sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719 \ - --hash=sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1 \ - --hash=sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178 \ - --hash=sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95 \ - --hash=sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96 \ - --hash=sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1 \ - --hash=sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a \ - --hash=sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588 \ - --hash=sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec \ - --hash=sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004 \ - --hash=sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780 \ - --hash=sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d \ - --hash=sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca \ - --hash=sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665 \ - --hash=sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296 \ - --hash=sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c \ - --hash=sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451 \ - --hash=sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3 +aiohttp==3.14.3 \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 # via # aiobotocore # litellm diff --git a/backend/requirements/ee.txt b/backend/requirements/ee.txt index 107e9a1a587..faf1e2b406a 100644 --- a/backend/requirements/ee.txt +++ b/backend/requirements/ee.txt @@ -20,72 +20,72 @@ aiohappyeyeballs==2.6.1 \ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 # via aiohttp -aiohttp==3.14.1 \ - --hash=sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983 \ - --hash=sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340 \ - --hash=sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d \ - --hash=sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a \ - --hash=sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4 \ - --hash=sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8 \ - --hash=sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397 \ - --hash=sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8 \ - --hash=sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09 \ - --hash=sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba \ - --hash=sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf \ - --hash=sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5 \ - --hash=sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035 \ - --hash=sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6 \ - --hash=sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35 \ - --hash=sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333 \ - --hash=sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203 \ - --hash=sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1 \ - --hash=sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251 \ - --hash=sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365 \ - --hash=sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94 \ - --hash=sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da \ - --hash=sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe \ - --hash=sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080 \ - --hash=sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397 \ - --hash=sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9 \ - --hash=sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345 \ - --hash=sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3 \ - --hash=sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602 \ - --hash=sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444 \ - --hash=sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15 \ - --hash=sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe \ - --hash=sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2 \ - --hash=sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496 \ - --hash=sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876 \ - --hash=sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448 \ - --hash=sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6 \ - --hash=sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd \ - --hash=sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f \ - --hash=sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c \ - --hash=sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca \ - --hash=sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c \ - --hash=sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa \ - --hash=sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc \ - --hash=sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0 \ - --hash=sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0 \ - --hash=sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844 \ - --hash=sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719 \ - --hash=sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1 \ - --hash=sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178 \ - --hash=sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95 \ - --hash=sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96 \ - --hash=sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1 \ - --hash=sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a \ - --hash=sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588 \ - --hash=sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec \ - --hash=sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004 \ - --hash=sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780 \ - --hash=sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d \ - --hash=sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca \ - --hash=sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665 \ - --hash=sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296 \ - --hash=sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c \ - --hash=sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451 \ - --hash=sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3 +aiohttp==3.14.3 \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 # via # aiobotocore # litellm diff --git a/backend/requirements/model_server.txt b/backend/requirements/model_server.txt index 94594bea648..2d915c307fc 100644 --- a/backend/requirements/model_server.txt +++ b/backend/requirements/model_server.txt @@ -23,72 +23,72 @@ aiohappyeyeballs==2.6.1 \ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 # via aiohttp -aiohttp==3.14.1 \ - --hash=sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983 \ - --hash=sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340 \ - --hash=sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d \ - --hash=sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a \ - --hash=sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4 \ - --hash=sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8 \ - --hash=sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397 \ - --hash=sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8 \ - --hash=sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09 \ - --hash=sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba \ - --hash=sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf \ - --hash=sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5 \ - --hash=sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035 \ - --hash=sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6 \ - --hash=sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35 \ - --hash=sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333 \ - --hash=sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203 \ - --hash=sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1 \ - --hash=sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251 \ - --hash=sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365 \ - --hash=sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94 \ - --hash=sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da \ - --hash=sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe \ - --hash=sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080 \ - --hash=sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397 \ - --hash=sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9 \ - --hash=sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345 \ - --hash=sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3 \ - --hash=sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602 \ - --hash=sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444 \ - --hash=sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15 \ - --hash=sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe \ - --hash=sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2 \ - --hash=sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496 \ - --hash=sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876 \ - --hash=sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448 \ - --hash=sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6 \ - --hash=sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd \ - --hash=sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f \ - --hash=sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c \ - --hash=sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca \ - --hash=sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c \ - --hash=sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa \ - --hash=sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc \ - --hash=sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0 \ - --hash=sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0 \ - --hash=sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844 \ - --hash=sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719 \ - --hash=sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1 \ - --hash=sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178 \ - --hash=sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95 \ - --hash=sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96 \ - --hash=sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1 \ - --hash=sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a \ - --hash=sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588 \ - --hash=sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec \ - --hash=sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004 \ - --hash=sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780 \ - --hash=sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d \ - --hash=sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca \ - --hash=sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665 \ - --hash=sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296 \ - --hash=sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c \ - --hash=sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451 \ - --hash=sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3 +aiohttp==3.14.3 \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 # via # aiobotocore # litellm diff --git a/backend/tests/daily/connectors/confluence/test_confluence_html_parser.py b/backend/tests/daily/connectors/confluence/test_confluence_html_parser.py new file mode 100644 index 00000000000..213ce5eca35 --- /dev/null +++ b/backend/tests/daily/connectors/confluence/test_confluence_html_parser.py @@ -0,0 +1,83 @@ +import os +import time +from unittest.mock import patch + +import pytest + +from onyx.configs.constants import DocumentSource +from onyx.connectors.confluence.connector import ConfluenceConnector +from onyx.connectors.credentials_provider import OnyxStaticCredentialsProvider +from onyx.connectors.models import Document +from onyx.file_processing.enums import HtmlBasedConnectorTransformLinksStrategy +from tests.daily.connectors.utils import load_all_from_connector +from tests.utils.secret_names import TestSecret + +_PARSER_REGRESSION_SPACE = "ParserReg" +_LINK_PAGE_TITLE = "HTML Parser Regression - Link Scope" +_TABLE_PAGE_TITLE = "HTML Parser Regression - Table Scope" +_LINK_TARGET = "https://example.com/parser-regression-target" + +pytestmark = pytest.mark.secrets(TestSecret.CONFLUENCE_ACCESS_TOKEN) + + +def _make_connector(access_token: str) -> ConfluenceConnector: + connector = ConfluenceConnector( + wiki_base=os.environ["CONFLUENCE_TEST_SPACE_URL"], + space=_PARSER_REGRESSION_SPACE, + is_cloud=True, + ) + connector.set_credentials_provider( + OnyxStaticCredentialsProvider( + None, + DocumentSource.CONFLUENCE, + { + "confluence_username": os.environ["CONFLUENCE_USER_NAME"], + "confluence_access_token": access_token, + }, + ) + ) + return connector + + +@pytest.fixture(scope="module") +def parser_regression_documents( + test_secrets: dict[TestSecret, str], +) -> dict[str, Document]: + connector = _make_connector( + test_secrets[TestSecret.CONFLUENCE_ACCESS_TOKEN].strip() + ) + with patch( + "onyx.file_processing.html_utils.HTML_BASED_CONNECTOR_TRANSFORM_LINKS_STRATEGY", + HtmlBasedConnectorTransformLinksStrategy.MARKDOWN, + ): + result = load_all_from_connector(connector, 0, time.time()) + + return {document.semantic_identifier: document for document in result.documents} + + +def test_confluence_html_link_scope( + parser_regression_documents: dict[str, Document], +) -> None: + document = parser_regression_documents[_LINK_PAGE_TITLE] + + assert document.sections[0].text == ( + f"LINK_BEFORE [LINK_TARGET_ONLY]({_LINK_TARGET}) " + "LINK_AFTER_MUST_NOT_BE_CLICKABLE\n" + "LINK_NEXT_PARAGRAPH_MUST_NOT_BE_CLICKABLE" + ) + + +def test_confluence_html_table_scope( + parser_regression_documents: dict[str, Document], +) -> None: + document = parser_regression_documents[_TABLE_PAGE_TITLE] + + assert document.sections[0].text == ( + "TABLE_BEFORE\n" + "\tHEADER_ALPHA\tHEADER_BETA\n" + "\tCELL_ALPHA\tCELL_BETA_LINE_1 CELL_BETA_LINE_2\n" + "TABLE_AFTER_HEADING\n" + "TABLE_AFTER_PARAGRAPH_MUST_BE_SEPARATE\n" + "- TABLE_AFTER_LIST_ONE\n" + "- TABLE_AFTER_LIST_TWO" + ) diff --git a/backend/tests/daily/connectors/highspot/test_highspot_connector.py b/backend/tests/daily/connectors/highspot/test_highspot_connector.py index bc1d4b8bfac..4dc224c64ac 100644 --- a/backend/tests/daily/connectors/highspot/test_highspot_connector.py +++ b/backend/tests/daily/connectors/highspot/test_highspot_connector.py @@ -12,10 +12,22 @@ from onyx.connectors.models import Document, HierarchyNode from tests.utils.secret_names import TestSecret -pytestmark = pytest.mark.secrets( - TestSecret.HIGHSPOT_KEY, - TestSecret.HIGHSPOT_SECRET, -) +# Since 2026-08-10 the Highspot API answers 403 for valid credentials, so no test +# here can pass. `-x` makes the failure abort the whole connector suite, so mark +# them xfail until we know the cause. Remove the mark once the API works again. +pytestmark = [ + pytest.mark.secrets( + TestSecret.HIGHSPOT_KEY, + TestSecret.HIGHSPOT_SECRET, + ), + pytest.mark.xfail( + reason=( + "Highspot API returns 403 for valid credentials since 2026-08-10. " + "Case raised with Highspot to confirm the cause." + ), + strict=False, + ), +] def load_test_data(file_name: str = "test_highspot_data.json") -> dict: diff --git a/backend/tests/daily/connectors/sharepoint/test_sharepoint_connector.py b/backend/tests/daily/connectors/sharepoint/test_sharepoint_connector.py index 483351803bd..af1890507c3 100644 --- a/backend/tests/daily/connectors/sharepoint/test_sharepoint_connector.py +++ b/backend/tests/daily/connectors/sharepoint/test_sharepoint_connector.py @@ -6,6 +6,9 @@ import pytest +from ee.onyx.external_permissions.sharepoint.permission_utils import ( + GET_SHAREPOINT_LIST_ITEM_ID_LABEL, +) from onyx.access.models import ExternalAccess from onyx.configs.constants import DocumentSource from onyx.connectors.models import ( @@ -18,6 +21,8 @@ from onyx.connectors.sharepoint.connector import ( SharepointAuthMethod, SharepointConnector, + _download_via_graph_api, + sleep_and_retry, ) from onyx.db.enums import HierarchyNodeType from tests.daily.connectors.utils import load_all_from_connector @@ -631,9 +636,19 @@ def test_sharepoint_connector_hierarchy_node_permissions( ) connector.load_credentials(sharepoint_cert_credentials) - with patch( - "onyx.connectors.sharepoint.connector.store_image_and_create_section", - mock_store_image, + with ( + patch( + "onyx.connectors.sharepoint.connector.store_image_and_create_section", + mock_store_image, + ), + patch( + "ee.onyx.external_permissions.sharepoint.permission_utils.sleep_and_retry", + wraps=sleep_and_retry, + ) as mock_permission_retry, + patch( + "onyx.connectors.sharepoint.connector._download_via_graph_api", + wraps=_download_via_graph_api, + ) as mock_graph_content_download, ): result = load_all_from_connector( connector, @@ -642,6 +657,13 @@ def test_sharepoint_connector_hierarchy_node_permissions( include_permissions=True, ) + assert result.documents + assert not any( + call.args[1] == GET_SHAREPOINT_LIST_ITEM_ID_LABEL + for call in mock_permission_retry.call_args_list + ) + mock_graph_content_download.assert_not_called() + site_node = find_hierarchy_node( result.hierarchy_nodes, HierarchyNodeType.SITE, diff --git a/backend/tests/daily/connectors/slack/conftest.py b/backend/tests/daily/connectors/slack/conftest.py index 9ccaefd1ba3..4c5ac266184 100644 --- a/backend/tests/daily/connectors/slack/conftest.py +++ b/backend/tests/daily/connectors/slack/conftest.py @@ -1,9 +1,7 @@ from collections.abc import Generator -from unittest.mock import MagicMock import pytest from pytest import FixtureRequest -from slack_sdk import WebClient from onyx.connectors.credentials_provider import OnyxStaticCredentialsProvider from onyx.connectors.slack.connector import SlackConnector @@ -11,16 +9,9 @@ from tests.utils.secret_names import TestSecret -@pytest.fixture -def mock_slack_client() -> MagicMock: - mock = MagicMock(spec=WebClient) - return mock - - @pytest.fixture def slack_connector( request: FixtureRequest, - mock_slack_client: MagicMock, slack_credentials_provider: OnyxStaticCredentialsProvider, ) -> Generator[SlackConnector]: channel: str | None = request.param if hasattr(request, "param") else None @@ -29,7 +20,6 @@ def slack_connector( channel_regex_enabled=False, use_redis=False, ) - connector.client = mock_slack_client connector.set_credentials_provider(credentials_provider=slack_credentials_provider) yield connector diff --git a/backend/tests/daily/connectors/slack/test_slack_connector.py b/backend/tests/daily/connectors/slack/test_slack_connector.py index 1df1a29cba2..3190fdb4a49 100644 --- a/backend/tests/daily/connectors/slack/test_slack_connector.py +++ b/backend/tests/daily/connectors/slack/test_slack_connector.py @@ -57,8 +57,8 @@ def test_indexing_channels_with_message_count( expected_messages: set[str], expected_channel_name: str, ) -> None: - if not slack_connector.client: - raise RuntimeError("Web client must be defined") + if not slack_connector.slack_client: + raise RuntimeError("Source operations gateway must be defined") result = load_all_from_connector( connector=slack_connector, @@ -114,8 +114,8 @@ def test_indexing_channels_with_message_count( def test_indexing_channels_that_dont_exist( slack_connector: SlackConnector, ) -> None: - if not slack_connector.client: - raise RuntimeError("Web client must be defined") + if not slack_connector.slack_client: + raise RuntimeError("Source operations gateway must be defined") with pytest.raises( ValueError, diff --git a/backend/tests/daily/connectors/slack/test_slack_perm_sync.py b/backend/tests/daily/connectors/slack/test_slack_perm_sync.py index db8d651f959..a4bc45bf3af 100644 --- a/backend/tests/daily/connectors/slack/test_slack_perm_sync.py +++ b/backend/tests/daily/connectors/slack/test_slack_perm_sync.py @@ -38,8 +38,8 @@ def test_load_from_checkpoint_access__public_channel( slack_connector: SlackConnector, ) -> None: """Test that load_from_checkpoint returns correct access information for documents.""" - if not slack_connector.client: - raise RuntimeError("Web client must be defined") + if not slack_connector.slack_client: + raise RuntimeError("Source operations gateway must be defined") docs = load_all_from_connector( connector=slack_connector, @@ -77,8 +77,8 @@ def test_load_from_checkpoint_access__private_channel( slack_connector: SlackConnector, ) -> None: """Test that load_from_checkpoint returns correct access information for documents.""" - if not slack_connector.client: - raise RuntimeError("Web client must be defined") + if not slack_connector.slack_client: + raise RuntimeError("Source operations gateway must be defined") docs = load_all_from_connector( connector=slack_connector, @@ -118,8 +118,8 @@ def test_slim_documents_access__public_channel( start_ts: float | None, ) -> None: """Test that retrieve_all_slim_docs_perm_sync returns correct access information for slim documents.""" - if not slack_connector.client: - raise RuntimeError("Web client must be defined") + if not slack_connector.slack_client: + raise RuntimeError("Source operations gateway must be defined") slim_docs_generator = slack_connector.retrieve_all_slim_docs_perm_sync( start=start_ts, @@ -154,8 +154,8 @@ def test_slim_documents_access__private_channel( slack_connector: SlackConnector, ) -> None: """Test that retrieve_all_slim_docs_perm_sync returns correct access information for slim documents.""" - if not slack_connector.client: - raise RuntimeError("Web client must be defined") + if not slack_connector.slack_client: + raise RuntimeError("Source operations gateway must be defined") slim_docs_generator = slack_connector.retrieve_all_slim_docs_perm_sync( start=None, diff --git a/backend/tests/external_dependency_unit/slack_bot/test_slack_bot_federated_search.py b/backend/tests/external_dependency_unit/slack_bot/test_slack_bot_federated_search.py index 4f24a7b8d17..30cc5d926c5 100644 --- a/backend/tests/external_dependency_unit/slack_bot/test_slack_bot_federated_search.py +++ b/backend/tests/external_dependency_unit/slack_bot/test_slack_bot_federated_search.py @@ -120,10 +120,10 @@ def _create_mock_slack_client( ) mock_users_info_response = Mock() - mock_users_info_response.__getitem__ = Mock( - side_effect=lambda key: {"ok": True}[key] - ) + # A real SlackResponse carries the whole parsed body (including ``ok``) in + # ``.data``, and its subscript access reads from it. mock_users_info_response.data = { + "ok": True, "user": { "id": "U9876543210", "name": "testuser", @@ -134,8 +134,11 @@ def _create_mock_slack_client( "last_name": "User", "email": "test@example.com", }, - } + }, } + mock_users_info_response.__getitem__ = Mock( + side_effect=mock_users_info_response.data.__getitem__ + ) mock_client.web_client.users_info = Mock(return_value=mock_users_info_response) mock_auth_test_response = { diff --git a/backend/tests/integration/connector_job_tests/slack/slack_api_utils.py b/backend/tests/integration/connector_job_tests/slack/slack_api_utils.py index cc6f8ab4d25..d5515f1a22a 100644 --- a/backend/tests/integration/connector_job_tests/slack/slack_api_utils.py +++ b/backend/tests/integration/connector_job_tests/slack/slack_api_utils.py @@ -9,15 +9,52 @@ - chat:write.public """ +from collections.abc import Callable, Generator from typing import Any, cast from uuid import uuid4 from slack_sdk import WebClient from slack_sdk.errors import SlackApiError +from slack_sdk.web import SlackResponse -from onyx.connectors.slack.connector import get_channel_messages -from onyx.connectors.slack.models import ChannelType -from onyx.connectors.slack.utils import make_paginated_slack_api_call +from onyx.connectors.slack.models import ChannelType, MessageType + +_SLACK_LIMIT = 900 + + +def make_paginated_slack_api_call( + call: Callable[..., SlackResponse], **kwargs: Any +) -> Generator[dict[str, Any], None, None]: + """Cursor pagination over the test-management client. + + Test setup drives its own admin ``WebClient`` (kicks members, deletes + messages), so it keeps a local paginator instead of going through the + connector's source-operations gateway. + """ + cursor: str | None = None + has_more = True + while has_more: + response = call(cursor=cursor, limit=_SLACK_LIMIT, **kwargs) + yield cast(dict[str, Any], response.validate()) + cursor = cast(dict[str, Any], response.get("response_metadata", {})).get( + "next_cursor", "" + ) + has_more = bool(cursor) + + +def get_channel_messages( + slack_client: WebClient, channel: ChannelType +) -> Generator[list[MessageType], None, None]: + """Yields message batches for a channel via the test-management client.""" + if not channel["is_member"]: + # Join only works for public channels; private membership needs an + # invite, handled by the test setup. + slack_client.conversations_join(channel=channel["id"]) + for result in make_paginated_slack_api_call( + slack_client.conversations_history, + channel=channel["id"], + ): + yield cast(list[MessageType], result["messages"]) def _get_slack_channel_id(channel: ChannelType) -> str: diff --git a/backend/tests/unit/ee/onyx/external_permissions/sharepoint/test_permission_utils.py b/backend/tests/unit/ee/onyx/external_permissions/sharepoint/test_permission_utils.py index b2cdded0981..34218822b6e 100644 --- a/backend/tests/unit/ee/onyx/external_permissions/sharepoint/test_permission_utils.py +++ b/backend/tests/unit/ee/onyx/external_permissions/sharepoint/test_permission_utils.py @@ -13,6 +13,7 @@ GroupsResult, _enumerate_ad_groups_paginated, _get_azuread_groups, + _get_sharepoint_list_item_id, _has_only_limited_access, _is_public_item, _iter_graph_collection, @@ -24,7 +25,10 @@ ) from onyx.access.models import ExternalAccess from onyx.background.indexing.checkpointing_utils import check_checkpoint_size -from onyx.connectors.sharepoint.connector import SharepointConnectorCheckpoint +from onyx.connectors.sharepoint.connector import ( + DriveItemData, + SharepointConnectorCheckpoint, +) from onyx.connectors.sharepoint.connector_utils import ( SharepointGroup, SharepointPermissionCache, @@ -70,6 +74,22 @@ def _make_sharepoint_group(name: str) -> SharepointGroup: ) +@patch(f"{MODULE}.sleep_and_retry") +def test_sharepoint_ids_avoid_list_item_lookup(mock_sleep_and_retry: MagicMock) -> None: + drive_item = DriveItemData.from_graph_json( + { + "id": "drive-item-id", + "name": "document.pdf", + "webUrl": "https://tenant.sharepoint.com/document.pdf", + "parentReference": {"driveId": "drive-id"}, + "sharepointIds": {"listItemId": "42"}, + } + ).to_sdk_driveitem(MagicMock()) + + assert _get_sharepoint_list_item_id(drive_item) == "42" + mock_sleep_and_retry.assert_not_called() + + @patch(f"{MODULE}._get_azuread_groups") def test_document_group_expansion_is_cached(mock_get_group: MagicMock) -> None: group = _make_ad_group("Engineering", "engineering-id") diff --git a/backend/tests/unit/ee/onyx/external_permissions/slack/test_grid_perm_sync.py b/backend/tests/unit/ee/onyx/external_permissions/slack/test_grid_perm_sync.py index 58a494be804..107aec8748f 100644 --- a/backend/tests/unit/ee/onyx/external_permissions/slack/test_grid_perm_sync.py +++ b/backend/tests/unit/ee/onyx/external_permissions/slack/test_grid_perm_sync.py @@ -18,6 +18,10 @@ from onyx.access.models import ExternalAccess from onyx.connectors.models import SlimDocument from onyx.connectors.slack.models import ChannelType +from onyx.connectors.slack.source_operations import ( + SlackChannelMembersPage, + SlackUsersPage, +) def _channel(channel_id: str, **overrides: Any) -> ChannelType: @@ -53,77 +57,84 @@ def _channel(channel_id: str, **overrides: Any) -> ChannelType: class TestFetchUserIdToEmailMap: def test_non_grid_calls_users_list_without_team_id(self) -> None: - client = MagicMock() - with patch( - "ee.onyx.external_permissions.slack.utils.make_paginated_slack_api_call" - ) as mock_paginate: - mock_paginate.return_value = iter( - [{"members": [{"id": "U1", "profile": {"email": "u1@x.com"}}]}] - ) - result = fetch_user_id_to_email_map(client) - assert result == {"U1": "u1@x.com"} - assert mock_paginate.call_count == 1 - assert "team_id" not in mock_paginate.call_args.kwargs + slack_client = MagicMock() + slack_client.list_users.return_value = iter( + [SlackUsersPage(members=[{"id": "U1", "profile": {"email": "u1@x.com"}}])] + ) + result = fetch_user_id_to_email_map(slack_client) + assert result == {"U1": "u1@x.com"} + assert slack_client.list_users.call_count == 1 + # The operation omits a None team_id from the API call. + assert slack_client.list_users.call_args.kwargs == {"team_id": None} def test_grid_iterates_each_team_with_team_id(self) -> None: - client = MagicMock() - with patch( - "ee.onyx.external_permissions.slack.utils.make_paginated_slack_api_call" - ) as mock_paginate: - mock_paginate.side_effect = [ - iter([{"members": [{"id": "U1", "profile": {"email": "u1@x.com"}}]}]), - iter([{"members": [{"id": "U2", "profile": {"email": "u2@x.com"}}]}]), - ] - result = fetch_user_id_to_email_map(client, team_ids=["T1", "T2"]) - assert result == {"U1": "u1@x.com", "U2": "u2@x.com"} - assert mock_paginate.call_count == 2 - assert mock_paginate.call_args_list[0].kwargs == {"team_id": "T1"} - assert mock_paginate.call_args_list[1].kwargs == {"team_id": "T2"} + slack_client = MagicMock() + slack_client.list_users.side_effect = [ + iter( + [ + SlackUsersPage( + members=[{"id": "U1", "profile": {"email": "u1@x.com"}}] + ) + ] + ), + iter( + [ + SlackUsersPage( + members=[{"id": "U2", "profile": {"email": "u2@x.com"}}] + ) + ] + ), + ] + result = fetch_user_id_to_email_map(slack_client, team_ids=["T1", "T2"]) + assert result == {"U1": "u1@x.com", "U2": "u2@x.com"} + assert slack_client.list_users.call_count == 2 + assert slack_client.list_users.call_args_list[0].kwargs == {"team_id": "T1"} + assert slack_client.list_users.call_args_list[1].kwargs == {"team_id": "T2"} class TestFetchTeamUserEmails: def test_returns_per_team_email_sets(self) -> None: - client = MagicMock() - with patch( - "onyx.connectors.slack.utils.make_paginated_slack_api_call" - ) as mock_paginate: - mock_paginate.side_effect = [ - iter([{"members": [{"id": "U1", "profile": {"email": "u1@x.com"}}]}]), - iter( - [ - { - "members": [ - {"id": "U2", "profile": {"email": "u2@x.com"}}, - {"id": "U3", "profile": {"email": "u3@x.com"}}, - ] - } - ] - ), - ] - result = fetch_team_user_emails(client, ["T1", "T2"]) - assert result == {"T1": {"u1@x.com"}, "T2": {"u2@x.com", "u3@x.com"}} - - def test_skips_users_without_email(self) -> None: - client = MagicMock() - with patch( - "onyx.connectors.slack.utils.make_paginated_slack_api_call" - ) as mock_paginate: - mock_paginate.return_value = iter( + slack_client = MagicMock() + slack_client.list_users.side_effect = [ + iter( [ - { - "members": [ - {"id": "U1", "profile": {"email": "u1@x.com"}}, - {"id": "U2", "profile": {}}, + SlackUsersPage( + members=[{"id": "U1", "profile": {"email": "u1@x.com"}}] + ) + ] + ), + iter( + [ + SlackUsersPage( + members=[ + {"id": "U2", "profile": {"email": "u2@x.com"}}, + {"id": "U3", "profile": {"email": "u3@x.com"}}, ] - } + ) ] - ) - assert fetch_team_user_emails(client, ["T1"]) == {"T1": {"u1@x.com"}} + ), + ] + result = fetch_team_user_emails(slack_client, ["T1", "T2"]) + assert result == {"T1": {"u1@x.com"}, "T2": {"u2@x.com", "u3@x.com"}} + + def test_skips_users_without_email(self) -> None: + slack_client = MagicMock() + slack_client.list_users.return_value = iter( + [ + SlackUsersPage( + members=[ + {"id": "U1", "profile": {"email": "u1@x.com"}}, + {"id": "U2", "profile": {}}, + ] + ) + ] + ) + assert fetch_team_user_emails(slack_client, ["T1"]) == {"T1": {"u1@x.com"}} class TestFetchChannelPermissionsGrid: def test_public_channel_scoped_to_its_workspace_users(self) -> None: - client = MagicMock() + slack_client = MagicMock() ws_emails = { "T_W1": {"a@x.com", "b@x.com", "c@x.com"}, "T_W2": {"z@x.com"}, @@ -136,7 +147,7 @@ def test_public_channel_scoped_to_its_workspace_users(self) -> None: mock_get.side_effect = [[ch_w1, ch_w2], []] # public, private workspace_perm = _fetch_workspace_permissions({"U1": "a@x.com"}) result = _fetch_channel_permissions( - slack_client=client, + slack_client=slack_client, workspace_permissions=workspace_perm, user_id_to_email_map={}, team_ids=["T_W1", "T_W2"], @@ -152,7 +163,7 @@ def test_public_channel_scoped_to_its_workspace_users(self) -> None: assert result["C_W2"].is_public is False def test_org_shared_public_channel_unions_users_across_workspaces(self) -> None: - client = MagicMock() + slack_client = MagicMock() ws_emails = { "T_W1": {"a@x.com", "b@x.com"}, "T_W2": {"z@x.com"}, @@ -169,7 +180,7 @@ def test_org_shared_public_channel_unions_users_across_workspaces(self) -> None: mock_get.side_effect = [[shared], []] workspace_perm = _fetch_workspace_permissions({}) result = _fetch_channel_permissions( - slack_client=client, + slack_client=slack_client, workspace_permissions=workspace_perm, user_id_to_email_map={}, team_ids=["T_W1", "T_W2"], @@ -184,7 +195,7 @@ def test_org_shared_public_channel_unions_users_across_workspaces(self) -> None: def test_public_channel_fallback_to_is_public_when_teams_unknown( self, ) -> None: - client = MagicMock() + slack_client = MagicMock() ws_emails = {"T_W1": {"a@x.com"}} ch = _channel("C_UNKNOWN", team="T_UNKNOWN") with patch( @@ -193,7 +204,7 @@ def test_public_channel_fallback_to_is_public_when_teams_unknown( mock_get.side_effect = [[ch], []] workspace_perm = _fetch_workspace_permissions({}) result = _fetch_channel_permissions( - slack_client=client, + slack_client=slack_client, workspace_permissions=workspace_perm, user_id_to_email_map={}, team_ids=["T_W1"], @@ -205,7 +216,7 @@ def test_public_channel_fallback_to_is_public_when_teams_unknown( def test_public_channel_fallback_to_is_public_when_union_exceeds_cap( self, ) -> None: - client = MagicMock() + slack_client = MagicMock() big = {f"u{i}@x.com" for i in range(ExternalAccess.MAX_NUM_ENTRIES + 1)} ws_emails = {"T_W1": big} ch = _channel("C_BIG", team="T_W1") @@ -215,7 +226,7 @@ def test_public_channel_fallback_to_is_public_when_union_exceeds_cap( mock_get.side_effect = [[ch], []] workspace_perm = _fetch_workspace_permissions({}) result = _fetch_channel_permissions( - slack_client=client, + slack_client=slack_client, workspace_permissions=workspace_perm, user_id_to_email_map={}, team_ids=["T_W1"], @@ -231,7 +242,7 @@ def test_non_grid_public_channel_not_overridden(self) -> None: the channel id is in ``channel_permissions``; non-Grid public channels are intentionally absent so the ingest value wins. """ - client = MagicMock() + slack_client = MagicMock() ch = _channel("C1") # no team field, non-Grid with patch( "ee.onyx.external_permissions.slack.doc_sync.get_channels" @@ -241,7 +252,7 @@ def test_non_grid_public_channel_not_overridden(self) -> None: {"U1": "a@x.com", "U2": "b@x.com"} ) result = _fetch_channel_permissions( - slack_client=client, + slack_client=slack_client, workspace_permissions=workspace_perm, user_id_to_email_map={}, team_ids=None, @@ -250,22 +261,19 @@ def test_non_grid_public_channel_not_overridden(self) -> None: assert "C1" not in result def test_channel_filter_limits_private_member_fetches(self) -> None: - client = MagicMock() + slack_client = MagicMock() included = _channel("C_INCLUDED", name="included", is_private=True) excluded = _channel("C_EXCLUDED", name="excluded", is_private=True) - with ( - patch( - "ee.onyx.external_permissions.slack.doc_sync.get_channels" - ) as mock_get, - patch( - "ee.onyx.external_permissions.slack.doc_sync.make_paginated_slack_api_call" - ) as mock_paginate, - ): + with patch( + "ee.onyx.external_permissions.slack.doc_sync.get_channels" + ) as mock_get: mock_get.side_effect = [[], [included, excluded]] # public, private - mock_paginate.return_value = iter([{"members": ["U1"]}]) + slack_client.list_channel_members.return_value = iter( + [SlackChannelMembersPage(members=["U1"])] + ) workspace_perm = _fetch_workspace_permissions({"U1": "u1@x.com"}) result = _fetch_channel_permissions( - slack_client=client, + slack_client=slack_client, workspace_permissions=workspace_perm, user_id_to_email_map={"U1": "u1@x.com"}, channels_to_include=["included"], @@ -273,25 +281,25 @@ def test_channel_filter_limits_private_member_fetches(self) -> None: assert set(result) == {"C_INCLUDED"} assert result["C_INCLUDED"].external_user_emails == {"u1@x.com"} - mock_paginate.assert_called_once() - assert mock_paginate.call_args.kwargs["channel"] == "C_INCLUDED" + slack_client.list_channel_members.assert_called_once() + assert ( + slack_client.list_channel_members.call_args.kwargs["channel_id"] + == "C_INCLUDED" + ) def test_channel_filter_skips_missing_included_channels(self) -> None: - client = MagicMock() + slack_client = MagicMock() included = _channel("C_INCLUDED", name="included", is_private=True) - with ( - patch( - "ee.onyx.external_permissions.slack.doc_sync.get_channels" - ) as mock_get, - patch( - "ee.onyx.external_permissions.slack.doc_sync.make_paginated_slack_api_call" - ) as mock_paginate, - ): + with patch( + "ee.onyx.external_permissions.slack.doc_sync.get_channels" + ) as mock_get: mock_get.side_effect = [[], [included]] # public, private - mock_paginate.return_value = iter([{"members": ["U1"]}]) + slack_client.list_channel_members.return_value = iter( + [SlackChannelMembersPage(members=["U1"])] + ) workspace_perm = _fetch_workspace_permissions({"U1": "u1@x.com"}) result = _fetch_channel_permissions( - slack_client=client, + slack_client=slack_client, workspace_permissions=workspace_perm, user_id_to_email_map={"U1": "u1@x.com"}, channels_to_include=["included", "missing"], @@ -299,26 +307,26 @@ def test_channel_filter_skips_missing_included_channels(self) -> None: assert set(result) == {"C_INCLUDED"} assert result["C_INCLUDED"].external_user_emails == {"u1@x.com"} - mock_paginate.assert_called_once() - assert mock_paginate.call_args.kwargs["channel"] == "C_INCLUDED" + slack_client.list_channel_members.assert_called_once() + assert ( + slack_client.list_channel_members.call_args.kwargs["channel_id"] + == "C_INCLUDED" + ) def test_grid_channel_filter_limits_private_member_fetches(self) -> None: - client = MagicMock() + slack_client = MagicMock() included = _channel("C_INCLUDED", name="included", is_private=True, team="T1") excluded = _channel("C_EXCLUDED", name="excluded", is_private=True, team="T1") - with ( - patch( - "ee.onyx.external_permissions.slack.doc_sync.get_channels_across_teams" - ) as mock_get, - patch( - "ee.onyx.external_permissions.slack.doc_sync.make_paginated_slack_api_call" - ) as mock_paginate, - ): + with patch( + "ee.onyx.external_permissions.slack.doc_sync.get_channels_across_teams" + ) as mock_get: mock_get.side_effect = [[], [included, excluded]] # public, private - mock_paginate.return_value = iter([{"members": ["U1"]}]) + slack_client.list_channel_members.return_value = iter( + [SlackChannelMembersPage(members=["U1"])] + ) workspace_perm = _fetch_workspace_permissions({"U1": "u1@x.com"}) result = _fetch_channel_permissions( - slack_client=client, + slack_client=slack_client, workspace_permissions=workspace_perm, user_id_to_email_map={"U1": "u1@x.com"}, team_ids=["T1"], @@ -329,8 +337,11 @@ def test_grid_channel_filter_limits_private_member_fetches(self) -> None: assert set(result) == {"C_INCLUDED"} assert result["C_INCLUDED"].external_user_emails == {"u1@x.com"} assert mock_get.call_count == 2 - mock_paginate.assert_called_once() - assert mock_paginate.call_args.kwargs["channel"] == "C_INCLUDED" + slack_client.list_channel_members.assert_called_once() + assert ( + slack_client.list_channel_members.call_args.kwargs["channel_id"] + == "C_INCLUDED" + ) class TestEEGetChannelAccessGrid: @@ -411,22 +422,19 @@ def test_public_channel_grid_falls_back_to_is_public_when_union_exceeds_cap( assert access.external_user_emails == set() def test_private_channel_uses_members_path_regardless_of_grid(self) -> None: - client = MagicMock() + slack_client = MagicMock() ch = _channel("C1", is_private=True, team="T1") - with ( - patch( - "ee.onyx.external_permissions.slack.channel_access.make_paginated_slack_api_call" - ) as mock_paginate, - patch( - "ee.onyx.external_permissions.slack.channel_access.expert_info_from_slack_id" - ) as mock_expert, - ): - mock_paginate.return_value = iter([{"members": ["U1", "U2"]}]) - mock_expert.side_effect = lambda user_id, client, user_cache: ( # noqa: ARG005 + with patch( + "ee.onyx.external_permissions.slack.channel_access.expert_info_from_slack_id" + ) as mock_expert: + slack_client.list_channel_members.return_value = iter( + [SlackChannelMembersPage(members=["U1", "U2"])] + ) + mock_expert.side_effect = lambda user_id, fetch_user_info, user_cache: ( # noqa: ARG005 MagicMock(email=f"{user_id.lower()}@x.com") if user_id else None ) access = ee_get_channel_access( - client, + slack_client, ch, {}, team_id_to_user_emails={"T1": {"a@x.com"}, "T2": {"z@x.com"}}, diff --git a/backend/tests/unit/onyx/server/token_rate_limits/__init__.py b/backend/tests/unit/ee/onyx/server/token_rate_limits/__init__.py similarity index 100% rename from backend/tests/unit/onyx/server/token_rate_limits/__init__.py rename to backend/tests/unit/ee/onyx/server/token_rate_limits/__init__.py diff --git a/backend/tests/unit/onyx/server/token_rate_limits/test_cost_budget_api.py b/backend/tests/unit/ee/onyx/server/token_rate_limits/test_cost_budget_api.py similarity index 97% rename from backend/tests/unit/onyx/server/token_rate_limits/test_cost_budget_api.py rename to backend/tests/unit/ee/onyx/server/token_rate_limits/test_cost_budget_api.py index e251b4e6530..84dfa3d5ef9 100644 --- a/backend/tests/unit/onyx/server/token_rate_limits/test_cost_budget_api.py +++ b/backend/tests/unit/ee/onyx/server/token_rate_limits/test_cost_budget_api.py @@ -12,12 +12,12 @@ from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import StaticPool +from ee.onyx.server.token_rate_limits.api import router as token_rate_limit_router from onyx.auth.users import current_user from onyx.db.engine.sql_engine import get_session from onyx.db.enums import Permission from onyx.db.models import TokenRateLimit from onyx.error_handling.exceptions import register_onyx_exception_handlers -from onyx.server.token_rate_limits.api import router as token_rate_limit_router class _StubUser: diff --git a/backend/tests/unit/onyx/connectors/cross_connector_utils/test_html_utils.py b/backend/tests/unit/onyx/connectors/cross_connector_utils/test_html_utils.py index 9ddc1941bb5..7c203b4d750 100644 --- a/backend/tests/unit/onyx/connectors/cross_connector_utils/test_html_utils.py +++ b/backend/tests/unit/onyx/connectors/cross_connector_utils/test_html_utils.py @@ -1,5 +1,9 @@ import pathlib +import pytest + +import onyx.file_processing.html_utils as html_utils +from onyx.file_processing.enums import HtmlBasedConnectorTransformLinksStrategy from onyx.file_processing.html_utils import parse_html_page_basic @@ -11,3 +15,35 @@ def test_parse_table() -> None: parsed = parse_html_page_basic(content) expected = "\n\thello\tthere\tgeneral\n\tkenobi\ta\tb\n\tc\td\te" assert expected in parsed + + +def test_content_after_table_uses_normal_block_formatting() -> None: + html = ( + "

before

" + "
cell
" + "

after heading

" + "

after paragraph

" + "" + ) + + assert parse_html_page_basic(html) == ( + "before\n\tcell\nafter heading\nafter paragraph\n- one\n- two" + ) + + +def test_markdown_link_ends_at_anchor( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + html_utils, + "HTML_BASED_CONNECTOR_TRANSFORM_LINKS_STRATEGY", + HtmlBasedConnectorTransformLinksStrategy.MARKDOWN, + ) + html = ( + '

See this link now.

' + "

Next paragraph.

" + ) + + assert parse_html_page_basic(html) == ( + "See [this link](https://example.com) now.\nNext paragraph." + ) diff --git a/backend/tests/unit/onyx/connectors/sharepoint/test_drive_matching.py b/backend/tests/unit/onyx/connectors/sharepoint/test_drive_matching.py index bcdf46d3fc2..b3fcde7da7d 100644 --- a/backend/tests/unit/onyx/connectors/sharepoint/test_drive_matching.py +++ b/backend/tests/unit/onyx/connectors/sharepoint/test_drive_matching.py @@ -9,6 +9,8 @@ from onyx.connectors.models import Document, DocumentSource, TextSection from onyx.connectors.sharepoint.connector import ( + DRIVE_ITEM_DOWNLOAD_URL_SELECT, + DRIVE_ITEM_SELECT_FIELDS, SHARED_DOCUMENTS_MAP, DriveItemData, SharepointConnector, @@ -435,13 +437,15 @@ def test_iter_drive_items_delta_uses_timestamp_token( connector = SharepointConnector() captured_urls: list[str] = [] + captured_params: list[dict[str, str] | None] = [] def fake_graph_api_get_json( self: SharepointConnector, # noqa: ARG001 url: str, - params: dict[str, str] | None = None, # noqa: ARG001 + params: dict[str, str] | None = None, ) -> dict[str, Any]: captured_urls.append(url) + captured_params.append(params) return { "value": [ { @@ -469,6 +473,8 @@ def fake_graph_api_get_json( assert items[0].id == "file-1" assert len(captured_urls) == 1 assert "token=2025-06-01T00%3A00%3A00%2B00%3A00" in captured_urls[0] + assert captured_params == [{"$top": "200", "$select": DRIVE_ITEM_SELECT_FIELDS}] + assert DRIVE_ITEM_DOWNLOAD_URL_SELECT in DRIVE_ITEM_SELECT_FIELDS.split(",") def test_iter_drive_items_delta_full_crawl_when_no_start( diff --git a/backend/tests/unit/onyx/connectors/slack/test_enterprise_grid.py b/backend/tests/unit/onyx/connectors/slack/test_enterprise_grid.py index 56540d2f812..73072227253 100644 --- a/backend/tests/unit/onyx/connectors/slack/test_enterprise_grid.py +++ b/backend/tests/unit/onyx/connectors/slack/test_enterprise_grid.py @@ -21,6 +21,10 @@ list_grid_team_ids, ) from onyx.connectors.slack.models import ChannelType, MessageType +from onyx.connectors.slack.source_operations import ( + SlackTeamInfoResponse, + SlackTeamsPage, +) from onyx.connectors.slack.utils import get_message_link @@ -64,61 +68,48 @@ def _msg(ts: str, thread_ts: str | None = None) -> MessageType: class TestListGridTeamIds: def test_returns_team_ids_from_paginated_response(self) -> None: - with patch( - "onyx.connectors.slack.connector.make_paginated_slack_api_call" - ) as mock_paginate: - mock_paginate.return_value = iter( - [ - {"teams": [{"id": "T1"}, {"id": "T2"}]}, - {"teams": [{"id": "T3"}]}, - ] - ) - client = MagicMock() - assert list_grid_team_ids(client) == ["T1", "T2", "T3"] + slack_client = MagicMock() + slack_client.list_teams.return_value = iter( + [ + SlackTeamsPage(teams=[{"id": "T1"}, {"id": "T2"}]), + SlackTeamsPage(teams=[{"id": "T3"}]), + ] + ) + assert list_grid_team_ids(slack_client) == ["T1", "T2", "T3"] def test_skips_teams_without_id(self) -> None: - with patch( - "onyx.connectors.slack.connector.make_paginated_slack_api_call" - ) as mock_paginate: - mock_paginate.return_value = iter( - [{"teams": [{"id": "T1"}, {"name": "no-id-team"}, {"id": ""}]}] - ) - assert list_grid_team_ids(MagicMock()) == ["T1"] + slack_client = MagicMock() + slack_client.list_teams.return_value = iter( + [SlackTeamsPage(teams=[{"id": "T1"}, {"name": "no-id-team"}, {"id": ""}])] + ) + assert list_grid_team_ids(slack_client) == ["T1"] def test_empty_response_returns_empty_list(self) -> None: - with patch( - "onyx.connectors.slack.connector.make_paginated_slack_api_call" - ) as mock_paginate: - mock_paginate.return_value = iter([{"teams": []}]) - assert list_grid_team_ids(MagicMock()) == [] + slack_client = MagicMock() + slack_client.list_teams.return_value = iter([SlackTeamsPage(teams=[])]) + assert list_grid_team_ids(slack_client) == [] class TestFetchTeamUrl: def test_returns_url_from_team_info(self) -> None: - client = MagicMock() - client.team_info.return_value = MagicMock( - get=lambda key, default=None: ( - {"id": "T1", "url": "https://acme.slack.com/"} - if key == "team" - else default - ) + slack_client = MagicMock() + slack_client.fetch_team_info.return_value = SlackTeamInfoResponse( + team={"id": "T1", "url": "https://acme.slack.com/"} ) - assert fetch_team_url(client, "T1") == "https://acme.slack.com/" - client.team_info.assert_called_once_with(team="T1") + assert fetch_team_url(slack_client, "T1") == "https://acme.slack.com/" + slack_client.fetch_team_info.assert_called_once_with(team_id="T1") def test_returns_none_when_slack_api_errors(self) -> None: - client = MagicMock() + slack_client = MagicMock() err_response = MagicMock() err_response.get.return_value = "team_not_found" - client.team_info.side_effect = SlackApiError("boom", err_response) - assert fetch_team_url(client, "T_BAD") is None + slack_client.fetch_team_info.side_effect = SlackApiError("boom", err_response) + assert fetch_team_url(slack_client, "T_BAD") is None def test_returns_none_when_url_missing(self) -> None: - client = MagicMock() - client.team_info.return_value = MagicMock( - get=lambda key, default=None: {} if key == "team" else default - ) - assert fetch_team_url(client, "T1") is None + slack_client = MagicMock() + slack_client.fetch_team_info.return_value = SlackTeamInfoResponse(team={}) + assert fetch_team_url(slack_client, "T1") is None class TestGetChannelsAcrossTeams: @@ -228,11 +219,9 @@ def test_non_grid_path_uses_workspace_url_unchanged(self) -> None: class TestGetMessageLinkTeamAware: def test_uses_per_team_url_when_provided(self) -> None: - client = MagicMock() - client.token = "xoxb-test" link = get_message_link( event=_msg("1700000000.000100"), - client=client, + slack_client=MagicMock(), channel_id="C1", team_id="T1", team_id_to_url={"T1": "https://team-one.slack.com"}, @@ -240,15 +229,13 @@ def test_uses_per_team_url_when_provided(self) -> None: assert link.startswith("https://team-one.slack.com/archives/C1/p") def test_falls_back_to_get_base_url_when_team_id_to_url_missing(self) -> None: - client = MagicMock() - client.token = "xoxb-fallback" with patch( "onyx.connectors.slack.utils.get_base_url", return_value="https://fallback.slack.com", ): link = get_message_link( event=_msg("1700000000.000100"), - client=client, + slack_client=MagicMock(), channel_id="C1", team_id="T1", team_id_to_url=None, @@ -256,11 +243,9 @@ def test_falls_back_to_get_base_url_when_team_id_to_url_missing(self) -> None: assert link.startswith("https://fallback.slack.com/archives/C1/p") def test_thread_ts_appended_when_present(self) -> None: - client = MagicMock() - client.token = "xoxb-thread" link = get_message_link( event=_msg("1700000001.000200", thread_ts="1700000000.000100"), - client=client, + slack_client=MagicMock(), channel_id="C1", team_id="T1", team_id_to_url={"T1": "https://team-one.slack.com"}, diff --git a/backend/tests/unit/onyx/connectors/slack/test_lazy_workspace_metadata.py b/backend/tests/unit/onyx/connectors/slack/test_lazy_workspace_metadata.py index 8aed322b04d..0c3d198f7cb 100644 --- a/backend/tests/unit/onyx/connectors/slack/test_lazy_workspace_metadata.py +++ b/backend/tests/unit/onyx/connectors/slack/test_lazy_workspace_metadata.py @@ -1,30 +1,41 @@ """Unit tests for lazy resolution of Slack workspace metadata. Workspace URL and Enterprise Grid topology are resolved when an indexing entry -point primes them, not when credentials are set. ``self.client`` serializes on a -Redis lock shared with running indexing jobs, so resolving eagerly let a -rate-limited indexing job block connector creation for the length of its -backoff. These tests pin the guarantee that the validation path never touches -that client, and that the exposed properties stay side-effect free. +point primes them, not when credentials are set. The coordinated client +serializes on a Redis lock shared with running indexing jobs, so resolving +eagerly lets a rate-limited indexing job block connector creation for the length +of its backoff. These tests pin the guarantee that the validation path never +touches that client, and that the exposed properties stay side-effect free. """ +from typing import Any from unittest.mock import MagicMock, patch import pytest -from slack_sdk.errors import SlackApiError from onyx.connectors.exceptions import InsufficientPermissionsError from onyx.connectors.slack.connector import SlackConnector +from onyx.connectors.slack.source_operations import SlackApiError _AUTH_NON_GRID = {"url": "https://workspace.slack.com"} _AUTH_GRID = {"url": "https://workspace.slack.com", "enterprise_id": "E1"} +def _slack_response(data: dict[str, Any]) -> MagicMock: + """ + Mimics a SlackResponse: ``validate()``, ``.data``, and mapping ``get``. + """ + response = MagicMock() + response.data = data + response.get.side_effect = data.get + return response + + def _set_credentials(connector: SlackConnector) -> tuple[MagicMock, MagicMock]: """Attaches mocked clients, returning ``(client, fast_client)``. - ``set_credentials_provider`` builds the coordinated client first and the - fast client second, so ``side_effect`` order matters here. + The gateway builds its clients lazily and memoizes them, so injecting mocks + into the memoization slots makes every operation run against them. """ client = MagicMock() fast_client = MagicMock() @@ -34,11 +45,10 @@ def _set_credentials(connector: SlackConnector) -> tuple[MagicMock, MagicMock]: provider.get_tenant_id.return_value = "public" provider.get_provider_key.return_value = "18" - with patch( - "onyx.connectors.slack.connector.WebClient", - side_effect=[client, fast_client], - ): - connector.set_credentials_provider(provider) + connector.set_credentials_provider(provider) + assert connector.slack_client is not None + connector.slack_client._cached_client = client + connector.slack_client._cached_fast_client = fast_client return client, fast_client @@ -54,10 +64,23 @@ def test_setting_credentials_does_not_call_auth_test(self) -> None: _, client, _ = _connector() client.auth_test.assert_not_called() + def test_setting_credentials_does_not_read_the_credential(self) -> None: + """Client construction is lazy, so the decrypt happens at first call.""" + connector = SlackConnector(channels=None, use_redis=False) + provider = MagicMock() + provider.get_tenant_id.return_value = "public" + provider.get_provider_key.return_value = "18" + + connector.set_credentials_provider(provider) + + provider.get_credentials.assert_not_called() + def test_validation_never_touches_the_coordinated_client(self) -> None: connector, client, fast_client = _connector() - fast_client.auth_test.return_value = {"ok": True} - fast_client.conversations_list.return_value = {"ok": True} + fast_client.auth_test.return_value = _slack_response({"ok": True}) + fast_client.conversations_list.return_value = _slack_response( + {"ok": True, "channels": []} + ) connector.validate_connector_settings() @@ -66,7 +89,7 @@ def test_validation_never_touches_the_coordinated_client(self) -> None: def test_properties_do_no_work_until_primed(self) -> None: connector, client, _ = _connector() - client.auth_test.return_value = _AUTH_NON_GRID + client.auth_test.return_value = _slack_response(_AUTH_NON_GRID) assert connector.workspace_url is None assert connector.grid_team_ids is None @@ -74,7 +97,7 @@ def test_properties_do_no_work_until_primed(self) -> None: def test_priming_resolves_and_result_is_cached(self) -> None: connector, client, _ = _connector() - client.auth_test.return_value = _AUTH_NON_GRID + client.auth_test.return_value = _slack_response(_AUTH_NON_GRID) connector._ensure_workspace_metadata() connector._ensure_workspace_metadata() @@ -93,13 +116,13 @@ def test_failed_resolution_raises_and_caches_nothing(self) -> None: assert connector._workspace_metadata is None client.auth_test.side_effect = None - client.auth_test.return_value = _AUTH_NON_GRID + client.auth_test.return_value = _slack_response(_AUTH_NON_GRID) connector._ensure_workspace_metadata() assert connector.workspace_url == "https://workspace.slack.com" def test_grid_team_enumeration_failure_raises(self) -> None: connector, client, _ = _connector() - client.auth_test.return_value = _AUTH_GRID + client.auth_test.return_value = _slack_response(_AUTH_GRID) with patch( "onyx.connectors.slack.connector.list_grid_team_ids", @@ -112,7 +135,7 @@ def test_grid_team_enumeration_failure_raises(self) -> None: def test_grid_user_email_failure_raises(self) -> None: connector, client, _ = _connector() - client.auth_test.return_value = _AUTH_GRID + client.auth_test.return_value = _slack_response(_AUTH_GRID) with ( patch( @@ -135,12 +158,14 @@ def test_grid_user_email_failure_raises(self) -> None: def test_new_credentials_invalidate_cached_metadata(self) -> None: connector, client, _ = _connector() - client.auth_test.return_value = _AUTH_NON_GRID + client.auth_test.return_value = _slack_response(_AUTH_NON_GRID) connector._ensure_workspace_metadata() assert connector.workspace_url == "https://workspace.slack.com" new_client, _ = _set_credentials(connector) - new_client.auth_test.return_value = {"url": "https://other.slack.com"} + new_client.auth_test.return_value = _slack_response( + {"url": "https://other.slack.com"} + ) connector._ensure_workspace_metadata() assert connector.workspace_url == "https://other.slack.com" @@ -158,7 +183,7 @@ def test_unconfigured_connector_reads_defaults_without_raising(self) -> None: class TestLazyGridResolution: def test_grid_topology_resolved_when_primed(self) -> None: connector, client, _ = _connector() - client.auth_test.return_value = _AUTH_GRID + client.auth_test.return_value = _slack_response(_AUTH_GRID) with ( patch( @@ -185,19 +210,35 @@ def test_grid_topology_resolved_when_primed(self) -> None: def test_validation_probes_users_scope_on_grid(self) -> None: connector, _, fast_client = _connector() - fast_client.auth_test.return_value = {"ok": True, "enterprise_id": "E1"} - fast_client.conversations_list.return_value = {"ok": True} - fast_client.auth_teams_list.return_value = {"teams": [{"id": "T1"}]} + fast_client.auth_test.return_value = _slack_response( + {"ok": True, "enterprise_id": "E1"} + ) + fast_client.conversations_list.return_value = _slack_response( + {"ok": True, "channels": []} + ) + fast_client.auth_teams_list.return_value = _slack_response( + {"teams": [{"id": "T1"}]} + ) + fast_client.users_list.return_value = _slack_response({"members": []}) connector.validate_connector_settings() - fast_client.users_list.assert_called_once_with(team_id="T1", limit=1) + # The gateway paginator supplies the cursor argument. + fast_client.users_list.assert_called_once_with( + cursor=None, limit=1, team_id="T1" + ) def test_validation_surfaces_missing_users_scope(self) -> None: connector, _, fast_client = _connector() - fast_client.auth_test.return_value = {"ok": True, "enterprise_id": "E1"} - fast_client.conversations_list.return_value = {"ok": True} - fast_client.auth_teams_list.return_value = {"teams": [{"id": "T1"}]} + fast_client.auth_test.return_value = _slack_response( + {"ok": True, "enterprise_id": "E1"} + ) + fast_client.conversations_list.return_value = _slack_response( + {"ok": True, "channels": []} + ) + fast_client.auth_teams_list.return_value = _slack_response( + {"teams": [{"id": "T1"}]} + ) error_response = MagicMock() error_response.get.side_effect = lambda key, default=None: { "error": "missing_scope", @@ -210,8 +251,10 @@ def test_validation_surfaces_missing_users_scope(self) -> None: def test_validation_skips_users_probe_off_grid(self) -> None: connector, _, fast_client = _connector() - fast_client.auth_test.return_value = {"ok": True} - fast_client.conversations_list.return_value = {"ok": True} + fast_client.auth_test.return_value = _slack_response({"ok": True}) + fast_client.conversations_list.return_value = _slack_response( + {"ok": True, "channels": []} + ) connector.validate_connector_settings() @@ -220,7 +263,7 @@ def test_validation_skips_users_probe_off_grid(self) -> None: def test_non_grid_workspace_skips_team_enumeration(self) -> None: connector, client, _ = _connector() - client.auth_test.return_value = _AUTH_NON_GRID + client.auth_test.return_value = _slack_response(_AUTH_NON_GRID) with patch("onyx.connectors.slack.connector.list_grid_team_ids") as list_teams: connector._ensure_workspace_metadata() diff --git a/backend/tests/unit/onyx/connectors/slack/test_slack_source_operations.py b/backend/tests/unit/onyx/connectors/slack/test_slack_source_operations.py new file mode 100644 index 00000000000..734ab6386d1 --- /dev/null +++ b/backend/tests/unit/onyx/connectors/slack/test_slack_source_operations.py @@ -0,0 +1,181 @@ +"""Pins the Slack source-operations gateway: inventory, variants, clients. + +The operation table is the living enumeration of every Slack remote interaction; +these tests keep it and the client-construction unification honest. +""" + +from typing import Any, cast +from unittest.mock import MagicMock + +import pytest + +from onyx.configs.constants import DocumentSource +from onyx.connectors.capabilities import CredentialCapability +from onyx.connectors.slack.source_operations import ( + OnyxSlackWebClient, + SlackSourceOperations, + SlackSourceOperationsConfig, +) +from onyx.connectors.source_operations import ( + OperationConsumes, + get_source_operations_class, +) + +_EXPECTED_OPERATIONS = { + "check_auth", + "list_channels", + "list_teams", + "fetch_team_info", + "join_channel", + "fetch_channel_history", + "fetch_thread_replies", + "fetch_channel_info", + "fetch_user_info", + "list_users", + "list_channel_members", + "list_usergroups", + "list_usergroup_members", +} + + +def _provider() -> MagicMock: + provider = MagicMock() + provider.get_credentials.return_value = {"slack_bot_token": "xoxb-test"} + provider.get_tenant_id.return_value = "public" + provider.get_provider_key.return_value = "18" + return provider + + +def _gateway(config: dict[str, Any] | None = None) -> SlackSourceOperations: + return SlackSourceOperations( + credentials_provider=_provider(), connector_specific_config=config + ) + + +def test_gateway_registers_for_slack() -> None: + # Postcondition (registration happened at import). + assert get_source_operations_class(DocumentSource.SLACK) is SlackSourceOperations + assert SlackSourceOperations.sdk_modules == ("slack_sdk",) + + +def test_operation_inventory_is_pinned() -> None: + """ + Pins the one-method-per-Slack-API-method inventory; a new remote interaction + must show up here, not in ad-hoc client code. + """ + # Precondition. + specs = SlackSourceOperations.operation_specs() + + # Postcondition. + assert set(specs) == _EXPECTED_OPERATIONS + # Every operation runs on the bot token alone. + assert all(spec.consumes == OperationConsumes.CREDENTIAL for spec in specs.values()) + # Checks land in the Slack capability checks PR; until then everything is + # untested with a reviewable reason. + assert all(spec.untested for spec in specs.values()) + + +def test_permission_class_variants_are_pinned() -> None: + """Verifies the public/private permission split on channel-scoped reads.""" + # Precondition. + specs = SlackSourceOperations.operation_specs() + + # Postcondition. + # Deliberately asserted as strings: call sites use ``SlackChannelVariant``, + # but these are the wire-level names the spec stores and the coverage + # harness counts. + for name in ("list_channels", "fetch_channel_history", "fetch_thread_replies"): + assert specs[name].variants == ("public", "private") + # ``conversations.info`` exists to discover the channel's privacy, so it + # cannot classify itself. + assert specs["fetch_channel_info"].variants == () + + +def test_capability_tags_are_pinned() -> None: + """ + Verifies which capabilities each operation serves, per real call sites. + """ + # Precondition. + specs = SlackSourceOperations.operation_specs() + + # Postcondition. + assert specs["fetch_user_info"].capabilities == { + CredentialCapability.INDEXING, + CredentialCapability.DOC_PERMISSION_SYNC, + CredentialCapability.EXTERNAL_GROUP_SYNC, + } + assert specs["list_channel_members"].capabilities == { + CredentialCapability.DOC_PERMISSION_SYNC + } + assert specs["list_usergroups"].capabilities == { + CredentialCapability.EXTERNAL_GROUP_SYNC + } + + +def test_variant_enforcement_fires_before_any_client_exists() -> None: + """ + Verifies a variant-bearing operation rejects unclassified calls at the + wrapper, before credential decrypt or client construction. + """ + # Precondition. + provider = _provider() + gateway = SlackSourceOperations(credentials_provider=provider) + + # Under test and postcondition. + # ``Any``-cast to exercise at runtime the call shape ty rejects statically. + with pytest.raises(TypeError, match="declares variants"): + cast(Any, gateway).list_channels(channel_types=["public_channel"]) + provider.get_credentials.assert_not_called() + + +def test_coordinated_client_uses_the_connector_key_forms() -> None: + """ + Pins the redis key forms of the unified client: EE perm-sync used to build + clients under bare-provider-key delay keys that never coordinated with + indexing; every consumer now shares these. + """ + # Under test. + client = _gateway()._client() + + # Postcondition. + assert isinstance(client, OnyxSlackWebClient) + assert client._delay_key == "connector:slack:credential_18:delay" + assert client._delay_lock == "connector:slack:credential_18:delay_lock" + + +def test_use_redis_false_builds_a_bare_client() -> None: + """Verifies the dev/test escape hatch skips redis coordination.""" + # Under test. + client = _gateway({"use_redis": False})._client() + + # Postcondition. + assert not isinstance(client, OnyxSlackWebClient) + + +def test_gateway_config_consumes_its_slice_of_the_connector_config() -> None: + """ + Verifies the raw connector config validates into the typed gateway config: + connector-only keys are ignored and coordination defaults to on. + """ + # Under test. + client = _gateway({"use_redis": False, "channels": ["general"]})._client() + + # Postcondition. + assert not isinstance(client, OnyxSlackWebClient) + assert SlackSourceOperationsConfig().use_redis is True + + +def test_clients_are_memoized_and_fast_client_is_separate() -> None: + """Verifies one coordinated client and one fast client per gateway.""" + # Precondition. + gateway = _gateway({"use_redis": False}) + + # Under test. + client = gateway._client() + fast = gateway._fast_client() + + # Postcondition. + assert gateway._client() is client + assert gateway._fast_client() is fast + assert fast is not client + assert fast.timeout == 1 diff --git a/bun.lock b/bun.lock index 5b97d84aaa2..03ae5d716ad 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "onyx", "devDependencies": { - "@typescript/native-preview": "^7.0.0-dev.20260706.1", + "@typescript/native-preview": "^7.0.0-dev.20260707.2", }, }, "desktop": { @@ -34,7 +34,7 @@ "@types/react-dom": "^19", "postcss": "^8.5.19", "tailwindcss": "^4.1.18", - "typescript": "^6", + "typescript": "^7", }, }, "widget": { @@ -43,12 +43,12 @@ "dependencies": { "dompurify": "^3.4.12", "lit": "^3.1.0", - "marked": "^18.0.5", + "marked": "^18.0.7", "terser": "^5.49.0", }, "devDependencies": { "@types/node": "^26.1.1", - "typescript": "^6.0.3", + "typescript": "^7.0.2", "vite": "^8.1.5", }, }, @@ -348,21 +348,61 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260706.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260706.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260706.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260706.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260706.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260706.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260706.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260706.1" }, "bin": { "tsgo": "bin/tsgo" } }, "sha512-lm3nzcj+cbml0Pt+0QyOpdhZES9WRj0Fv/fds2GaKSf9wXe8694gy6Fa5D65lRcP6q4HHivlrNqJo8rR5Klzww=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260707.2", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260707.2", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260707.2" }, "bin": { "tsgo": "bin/tsgo" } }, "sha512-oUGp+Rep/hqMhPunyinsALUwSlzHINSxitifPiSaeqoKOKD2OlR9NE3TaPqwsl4NlGslsOSUXI1JotWQzpYCPg=="], - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260706.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-l9UTIdxt99UUJ7pe/xWWwjoxr9EnG+Sc2k6FUchQrSK0sFZOlx76+hQm6DvuzWOr+1JEpVP/LGmnx7+qk8Yr3Q=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wny2pgKjGbiZtnOIHVa3tXC1UfDqxNEFzyPGmiqybedG8hipG2Nfp0l5UxbaKCjkLacUpH/W5bP2hBOMVhCOzg=="], - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260706.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-2/ivq1yeOrhNnN+aQhCLtGeqZ90R5v9OaoON4tG0zEJZLy+jIZGnRK6+gbI8lBhKuIVY2pjpHPJxN1VCaBmxGA=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260707.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Afc7M5zOwo+GpfcYwz5Z8HMB2tPVsui7nNIqEuuFB73MPdVqNn/Wmpe4tP4MRri0AtJnJknoHBaTJ/VDAp/Jhw=="], - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260706.1", "", { "os": "linux", "cpu": "arm" }, "sha512-E2n8tnUcXwY4vRmyEpS8CfpJbK7y5coPtKtqDFtGZHbvWLPoiIP6OhHF5fIPCNMziBclz+5MR86OzxK1jt93YQ=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "arm" }, "sha512-hJm/UOqZTr9FHmR7uNm8VGX4oKtfWk0Jem0zPeJFNC8ckGUfSBueyiEYMZB+XmRc1aG4x1E46y3CplP4CLHvGQ=="], - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260706.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-eokiB0dLt8uUeWH2BbwhQZLrGay5quzptTsFs8kh/nFdSF4gYLoBwmpJn/o5YYrQU89T7Mvi4tKPkgo0TCnwIw=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-iITBa2WjjTI5N9t5l7Z4KoOSI+2zBlhbvFzsD/f8qX8QoKjz/Y4DPyBDgezYi8nkqjjksbgSOJ3/ykzhwrB9cg=="], - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260706.1", "", { "os": "linux", "cpu": "x64" }, "sha512-JwrLUanCkVq+vbSjcc+qMqQN909Jhiy1s671q5vaYmBMLIzlhNSkvWqmHk+8XLt100zcgJDGgv+kHRazDlpPUQ=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "x64" }, "sha512-du0dzi6y97Po5vDNdPJTyyijHCpaS22JLRnKZEJXBDaO9gCIymOv/5QQokFRuOlQm0bWl3i9PF4OVdGP6uAOQA=="], - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260706.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-z75g24yUxktDSnP41t5Y3lV8BbraGyFuRvxhpd5GwXxT/qKQ3bHHcmdTTDSLkn+SRD8fAnnbFqb274UIC4OcCA=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260707.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SsAwfhyHJ1akgBc+99z4+hwdbHsdWaKB8EwCNIMA6JfSLMeUjffrYvxu+vfMyxVtOVOz7RrRXRoiDiu4a2sCtg=="], - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260706.1", "", { "os": "win32", "cpu": "x64" }, "sha512-kWP0dMeCvTN5E4X4lQ7dELxyarnfapY1R9KPwHZdN7mcY67uPYSFTvBHD1+V3Q4dTQHQVIaHDUNOfU6McBo1wA=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260707.2", "", { "os": "win32", "cpu": "x64" }, "sha512-DL4u27stv0fo71sVhOzHSwE+YMZsbBijVI+kg5dLDLilSH79WFTJ8RSQ46vJrCMt+Gjlv/JOZP1PuLJDfioYeQ=="], + + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], @@ -494,7 +534,7 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "marked": ["marked@18.0.5", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w=="], + "marked": ["marked@18.0.7", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], @@ -626,7 +666,7 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], diff --git a/examples/widget/package.json b/examples/widget/package.json index 2eb1a81b83e..1764dad1a9a 100644 --- a/examples/widget/package.json +++ b/examples/widget/package.json @@ -20,6 +20,6 @@ "@types/react-dom": "^19", "postcss": "^8.5.19", "tailwindcss": "^4.1.18", - "typescript": "^6" + "typescript": "^7" } } diff --git a/package.json b/package.json index 75c792fc854..d29d7a9dcb9 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,6 @@ "packageManager": "bun@1.3.13", "workspaces": ["widget", "examples/widget", "desktop"], "devDependencies": { - "@typescript/native-preview": "^7.0.0-dev.20260706.1" + "@typescript/native-preview": "^7.0.0-dev.20260707.2" } } diff --git a/pyproject.toml b/pyproject.toml index e9910d3cabf..40d8265304a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ [dependency-groups] backend = [ - "aiohttp==3.14.1", + "aiohttp==3.14.3", "alembic==1.18.4", "asyncpg==0.30.0", "atlassian-python-api==4.0.7", @@ -92,7 +92,7 @@ backend = [ "python-docx==1.1.2", "python-gitlab==5.6.0", "python-pptx==0.6.23", - "pypdf==6.14.2", + "pypdf==6.15.0", "pypdfium2==5.9.0", "python-dotenv==1.2.2", "python-slugify==8.0.4", diff --git a/uv.lock b/uv.lock index bc6521a64d8..d27dfd39bd0 100644 --- a/uv.lock +++ b/uv.lock @@ -117,7 +117,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.14.1" +version = "3.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -128,72 +128,72 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, - { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, - { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, - { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, - { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, - { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, - { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, - { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, - { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, - { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, - { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, - { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, - { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, - { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, - { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, - { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, - { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, - { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, - { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, - { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, - { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, - { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, - { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, - { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, - { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, - { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, - { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, - { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, - { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, - { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, - { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, - { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, ] [[package]] @@ -2044,14 +2044,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.57" +version = "3.1.58" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/0d/132ed135c871b6bf91adf16a0e43797cd535b81d4973b5d09291c54fc5ee/gitpython-3.1.57.tar.gz", hash = "sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488", size = 225898, upload-time = "2026-07-26T07:33:26.351Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/d6/5f358ff283325580c2003a6d953aea18cfe10ae87b46f5ebc80fa3a386dc/gitpython-3.1.58.tar.gz", hash = "sha256:621416df10ef3fd0e19fabf9172ddeed0fa704d353d04f194eec56a625a95b22", size = 228498, upload-time = "2026-08-04T15:05:49.47Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/6e/2139de986d9c7c3ac86f1f8be43858ce90bdfe2f7175e6c80c650ba15242/gitpython-3.1.57-py3-none-any.whl", hash = "sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf", size = 217151, upload-time = "2026-07-26T07:33:24.838Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/9d8752098bc442f0726e64aa6135940b3a96809915d1aa4206c1bb97881d/gitpython-3.1.58-py3-none-any.whl", hash = "sha256:d331e722577f0fd7fc1f857419b3ecc07af66282b933d2a4d95f84a042fdd50f", size = 220183, upload-time = "2026-08-04T15:05:48.025Z" }, ] [[package]] @@ -2394,15 +2394,15 @@ wheels = [ [[package]] name = "h2" -version = "4.3.0" +version = "4.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "hpack" }, { name = "hyperframe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" }, ] [[package]] @@ -2446,11 +2446,11 @@ wheels = [ [[package]] name = "hpack" -version = "4.1.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, ] [[package]] @@ -4100,7 +4100,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "sys_platform != 'win32'" }, + { name = "nvidia-cublas-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, @@ -4111,7 +4111,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, @@ -4138,9 +4138,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "sys_platform != 'win32'" }, - { name = "nvidia-cusparse-cu12", marker = "sys_platform != 'win32'" }, - { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" }, + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, @@ -4151,7 +4151,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, @@ -4451,7 +4451,7 @@ requires-dist = [ [package.metadata.requires-dev] backend = [ - { name = "aiohttp", specifier = "==3.14.1" }, + { name = "aiohttp", specifier = "==3.14.3" }, { name = "alembic", specifier = "==1.18.4" }, { name = "asana", specifier = "==5.0.8" }, { name = "asyncpg", specifier = "==0.30.0" }, @@ -4511,7 +4511,7 @@ backend = [ { name = "pyairtable", specifier = "==3.0.1" }, { name = "pycryptodome", specifier = "==3.19.1" }, { name = "pygithub", specifier = "==2.5.0" }, - { name = "pypdf", specifier = "==6.14.2" }, + { name = "pypdf", specifier = "==6.15.0" }, { name = "pypdfium2", specifier = "==5.9.0" }, { name = "python-dateutil", specifier = "==2.9.0.post0" }, { name = "python-docx", specifier = "==1.1.2" }, @@ -4946,7 +4946,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "sys_platform != 'win32'" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -5635,11 +5635,11 @@ wheels = [ [[package]] name = "pypdf" -version = "6.14.2" +version = "6.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/72/7dfd5ff1c9c37de97a731701f51af091325f123d9d4270361c9c69e4431f/pypdf-6.14.2.tar.gz", hash = "sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25", size = 6491182, upload-time = "2026-06-23T14:18:30.859Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/17/ee75a92718ec7212de831e71454d702225aa5e474a805cce169806044453/pypdf-6.15.0.tar.gz", hash = "sha256:d39c4d955a76409284a905e2d65b40076d77ab76129e0faaeeb6612403ecfc79", size = 6993794, upload-time = "2026-08-06T13:06:49.929Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/e6/136aa8993a2ae7214e0b0ef2edaa0d2e08d1d4e4982635b08a835ff31ec8/pypdf-6.14.2-py3-none-any.whl", hash = "sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946", size = 349514, upload-time = "2026-06-23T14:18:28.867Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/ce3067ac31e214a66388159f8462ddb8c13dd00170f24d555a1f1ae8ee91/pypdf-6.15.0-py3-none-any.whl", hash = "sha256:14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee", size = 378123, upload-time = "2026-08-06T13:06:47.709Z" }, ] [[package]] @@ -6582,8 +6582,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "sys_platform != 'win32'" }, - { name = "jeepney", marker = "sys_platform != 'win32'" }, + { name = "cryptography" }, + { name = "jeepney" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ diff --git a/web/.oxlintrc.json b/web/.oxlintrc.json index b605d03367e..764ed1b3fc9 100644 --- a/web/.oxlintrc.json +++ b/web/.oxlintrc.json @@ -1,6 +1,9 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["typescript", "unicorn", "oxc", "react", "nextjs"], + "jsPlugins": [ + { "name": "react-doctor", "specifier": "oxlint-plugin-react-doctor" } + ], "categories": { "correctness": "error" }, @@ -9,7 +12,15 @@ "react/exhaustive-deps": "off", "typescript/no-unused-vars": "off", "unicorn/no-thenable": "off", - "no-unused-vars": "off" + "no-unused-vars": "off", + "react-doctor/rules-of-hooks": "error", + "react-doctor/no-unguarded-browser-global-in-render-or-hook-init": "error", + "react-doctor/effect-needs-cleanup": "error", + "react-doctor/no-effect-with-fresh-deps": "error", + "react-doctor/aria-role": "error", + "react-doctor/no-ref-current-in-render": "error", + "react-doctor/no-impure-state-updater": "error", + "react-doctor/no-layout-property-animation": "error" }, "ignorePatterns": [ ".next", diff --git a/web/Dockerfile b/web/Dockerfile index 45ce08a89b8..cfa15db7648 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -88,6 +88,9 @@ ENV SENTRY_RELEASE=${SENTRY_RELEASE} # Add NODE_OPTIONS argument ARG NODE_OPTIONS +ARG SKIP_TYPE_CHECK +ENV SKIP_TYPE_CHECK=${SKIP_TYPE_CHECK} + # SENTRY_AUTH_TOKEN is injected via BuildKit secret mount so it is never written # to any image layer, build cache, or registry manifest. # Use NODE_OPTIONS in the build command diff --git a/web/bun.lock b/web/bun.lock index 7a2226fe201..b332e2fdfa1 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -48,10 +48,10 @@ "linguist-languages": "^9.3.1", "lodash": "^4.18.1", "lowlight": "^3.3.0", - "lucide-react": "^1.16.0", + "lucide-react": "^1.25.0", "mime": "^4.1.0", "motion": "^12.42.2", - "next": "16.2.11", + "next": "16.3.0", "next-themes": "^0.4.4", "postcss": "^8.5.10", "posthog-js": "^1.376.3", @@ -110,9 +110,10 @@ "jest-environment-jsdom": "^30.2.0", "oxfmt": "0.59.0", "oxlint": "^1.66.0", + "oxlint-plugin-react-doctor": "0.9.11", "stats.js": "^0.17.0", "storybook": "10.5.0", - "tailwindcss": "^4.3.0", + "tailwindcss": "^4.3.3", "ts-unused-exports": "^11.0.1", "tsup": "^8.5.1", "typescript": "^5.9.3", @@ -281,11 +282,11 @@ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], - "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], - "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], @@ -391,7 +392,7 @@ "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="], @@ -493,23 +494,23 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], - "@next/env": ["@next/env@16.2.11", "", {}, "sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA=="], + "@next/env": ["@next/env@16.3.0", "", {}, "sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA=="], "@onyx-ai/opal": ["@onyx-ai/opal@workspace:lib/opal"], @@ -529,47 +530,47 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], - "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.127.0", "", { "os": "android", "cpu": "arm" }, "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.142.0", "", { "os": "android", "cpu": "arm" }, "sha512-ZiRGDutGsv1G6bL/ozy/koC0Sv39T1DqyoC4KD1DOy9ZoACm1O5UWhEK2c02Qdk+4lfLVkvFa/mQ0fm/4h1BtQ=="], - "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.127.0", "", { "os": "android", "cpu": "arm64" }, "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg=="], + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.142.0", "", { "os": "android", "cpu": "arm64" }, "sha512-WZkvGRLNQTz8lR9zP5nLjUdlroRCopBu3g9zF1p/laE6DzT1UbQo8Rdz5MWhaJUPYg/6gp+jo7HUgsyKaN1FtQ=="], - "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.127.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg=="], + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.142.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-l4khS8LQOOVYsGRVARo1gSaCT/aBSceUVXgtovWc2+drnxVuDr082WA3OCHVdVzIz5JIrP/y9CWsSKxBDNmYGg=="], - "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.127.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw=="], + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.142.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-QBsNF3nqlXmcH2B1YOPqQYmCJoy4HuIjUxGbBO/k5JAJUl68ghU2psRY2zPk+RyBaWqKP/qfL4oaFgEMCdwskA=="], - "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.127.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA=="], + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.142.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-b7Q7m4Cqc6XqNhri3R+QhU+GVy646Pn+bkdhrDdWym/Fdi0ZUa+d73H9dm5H91JtbtAQ/z1d8XKMW3oOV8a4tQ=="], - "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ=="], + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.142.0", "", { "os": "linux", "cpu": "arm" }, "sha512-3riVS5IhdH3uCZj1Y9ftDQlR0dvLsIlw/edrRqk8JhgNd5K0XSs+UBtgh50N13CAlW9/TXj6sVGXaKNBocd0Yg=="], - "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g=="], + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.142.0", "", { "os": "linux", "cpu": "arm" }, "sha512-NmXUOpgpTSkhl795TiXmWppTwmSJ92RC1qvD6e4XOF+slgmo3e6Ah+kEu+6AN8s7NAOEwqGmir58MgSQSWmBSA=="], - "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ=="], + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.142.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gc0EXsKtXgerujmU2Bql3u1L1HsSQ2774R83idq/FoNMPVV/RY/1ErFsvnit7KoiP/sLvzQixeUo4Ut0ic0wmw=="], - "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA=="], + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.142.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-F2XvmWSE0uWpie+jHKKIFgdVOe9ypGhkEZxKx5DuW215K6cbAC274yYaPkcM7EqY4Df3Weyhpcz3lsURyH2LVg=="], - "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.127.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ=="], + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.142.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-wLMbT21U/QxknQsk+VvNF0b9D2/aGWhcaQQQ+VYlE8FwD5+GoWZIPPXNzyHmkYyhm0KB3itL+TBavjMatqNnYA=="], - "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ=="], + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.142.0", "", { "os": "linux", "cpu": "none" }, "sha512-+G8F/4ckwT7FCJV4H2bt09xEzJbjNCfuL4Sp1AYNaFtFMVtgIGMuJlteT82U+K0UIZ/DzAR/LDlMFnEuajG7Kw=="], - "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g=="], + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.142.0", "", { "os": "linux", "cpu": "none" }, "sha512-hTsHtTLxMAfCo+rpF5K3qZJKW2NpPN/CHd4mYB3y7XlSdspHkd2gehDIofP64AacA9nWQw2tY3O7wR6UY8IVOA=="], - "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.127.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q=="], + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.142.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-6y7qYY3TCUDYjqswImdTGl92y+KA/80twALegQPN27kfY+bG7Ib1+L3jbmrCZQx6wrVnai9IPsEZp07I0hx7JQ=="], - "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ=="], + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.142.0", "", { "os": "linux", "cpu": "x64" }, "sha512-i69kAWU+2LgoH5bR+zWiiu+UzAw7Oxkwv7COeJTeY19pn4e70nKQcr9Pm6cL2Z0Z54d+gl9qADlK/0yyuCPiBA=="], - "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg=="], + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.142.0", "", { "os": "linux", "cpu": "x64" }, "sha512-4SQs678MmjYVrmhAgCWD4o0vpaFszXw9xLX5p2Z9MMFcltxiLkA88wQjh80YHjPrXtpyZ2CWI5m+1yNKM0m2Pw=="], - "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.127.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ=="], + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.142.0", "", { "os": "none", "cpu": "arm64" }, "sha512-YHpx9N7Ln3a++Tc8rv+H7mrK1zyJQOAwCFg8LZ3lTs1T5afGWeZrLPhPT9HLnIwSjCyJqPWVMIrMxbjcmBr2oQ=="], - "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.127.0", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ=="], + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.142.0", "", { "dependencies": { "@emnapi/core": "1.11.2", "@emnapi/runtime": "1.11.2", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-3pLDyY3+oogW73RM5uehNgAiR/Xfb7fvO2Q1Z1gIqZ2+50XDVQmBVlRkHXZTU4gKnQHpwETNsYQVsJ3joVB2iA=="], - "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.127.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw=="], + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.142.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Had/VeVY28Oyb0K+Q4FV8KCzoBycIh93oDK6pCbya9lkzdq+ikMHMgBubsdqqlybjJmQRawCQRrnBRHyQwYvcQ=="], - "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.127.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw=="], + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.142.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-GGi3+YphVHavvgs6gum2UXoNCqzHAmPt/nXkn8ZQZstV2Q1qZD1Mn8fz/nWrDkefHQtrG/+1/XrbMxsBTo6Svw=="], - "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="], + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.142.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Ny/Wv4Us1LGC/ljwNTp+Hx3r/pH15EFfeDF0p+n898gt+TtRd6C9SccHcuUhDiNTb8s5tt7jdeAMDRQZ4Vq6hg=="], - "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + "@oxc-project/types": ["@oxc-project/types@0.142.0", "", {}, "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ=="], "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="], @@ -893,6 +894,8 @@ "@sentry/webpack-plugin": ["@sentry/webpack-plugin@5.3.0", "", { "dependencies": { "@sentry/bundler-plugin-core": "5.3.0" }, "peerDependencies": { "webpack": ">=5.0.0" } }, "sha512-i3OQUrS0FZlXLgq57RIKDp+vHHzuvYKPCKewAPXULWKMsBXFGhP6veGRQ+6To/pmZkkXjEX5ofVNDy9C3jEPKQ=="], + "@shaderfrog/glsl-parser": ["@shaderfrog/glsl-parser@7.0.1", "", {}, "sha512-8mpfsoPeRhesY3pOrzNZBL8uG6N5GVX1EHLBYbd4gzKs+c7vaEIqpTNK5VrffU33qQN4cwpP2v3u4aPPBU32sw=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], @@ -1051,6 +1054,8 @@ "@types/doctrine": ["@types/doctrine@0.0.9", "", {}, "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA=="], + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -1117,6 +1122,8 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.67.0", "", {}, "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww=="], + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], @@ -1533,7 +1540,9 @@ "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], @@ -1543,7 +1552,7 @@ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], @@ -1849,29 +1858,29 @@ "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], @@ -1899,7 +1908,7 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@1.21.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ=="], + "lucide-react": ["lucide-react@1.25.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -2047,7 +2056,7 @@ "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "next": ["next@16.2.11", "", { "dependencies": { "@next/env": "16.2.11", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.11", "@next/swc-darwin-x64": "16.2.11", "@next/swc-linux-arm64-gnu": "16.2.11", "@next/swc-linux-arm64-musl": "16.2.11", "@next/swc-linux-x64-gnu": "16.2.11", "@next/swc-linux-x64-musl": "16.2.11", "@next/swc-win32-arm64-msvc": "16.2.11", "@next/swc-win32-x64-msvc": "16.2.11", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ=="], + "next": ["next@16.3.0", "", { "dependencies": { "@next/env": "16.3.0", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.0", "@next/swc-darwin-x64": "16.3.0", "@next/swc-linux-arm64-gnu": "16.3.0", "@next/swc-linux-arm64-musl": "16.3.0", "@next/swc-linux-x64-gnu": "16.3.0", "@next/swc-linux-x64-musl": "16.3.0", "@next/swc-win32-arm64-msvc": "16.3.0", "@next/swc-win32-x64-msvc": "16.3.0", "sharp": "^0.35.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], @@ -2079,7 +2088,7 @@ "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "oxc-parser": ["oxc-parser@0.127.0", "", { "dependencies": { "@oxc-project/types": "^0.127.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.127.0", "@oxc-parser/binding-android-arm64": "0.127.0", "@oxc-parser/binding-darwin-arm64": "0.127.0", "@oxc-parser/binding-darwin-x64": "0.127.0", "@oxc-parser/binding-freebsd-x64": "0.127.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", "@oxc-parser/binding-linux-arm64-musl": "0.127.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-musl": "0.127.0", "@oxc-parser/binding-openharmony-arm64": "0.127.0", "@oxc-parser/binding-wasm32-wasi": "0.127.0", "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA=="], + "oxc-parser": ["oxc-parser@0.142.0", "", { "dependencies": { "@oxc-project/types": "^0.142.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.142.0", "@oxc-parser/binding-android-arm64": "0.142.0", "@oxc-parser/binding-darwin-arm64": "0.142.0", "@oxc-parser/binding-darwin-x64": "0.142.0", "@oxc-parser/binding-freebsd-x64": "0.142.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.142.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.142.0", "@oxc-parser/binding-linux-arm64-gnu": "0.142.0", "@oxc-parser/binding-linux-arm64-musl": "0.142.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.142.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.142.0", "@oxc-parser/binding-linux-riscv64-musl": "0.142.0", "@oxc-parser/binding-linux-s390x-gnu": "0.142.0", "@oxc-parser/binding-linux-x64-gnu": "0.142.0", "@oxc-parser/binding-linux-x64-musl": "0.142.0", "@oxc-parser/binding-openharmony-arm64": "0.142.0", "@oxc-parser/binding-wasm32-wasi": "0.142.0", "@oxc-parser/binding-win32-arm64-msvc": "0.142.0", "@oxc-parser/binding-win32-ia32-msvc": "0.142.0", "@oxc-parser/binding-win32-x64-msvc": "0.142.0" } }, "sha512-kKR+jPiRJYJDexVoziIg/FVGvr1fT1FZSSJOk6tVoMKKSlsf1Cso+cgGCJkOEDWOP174vRntCPFKg+AS7InWvw=="], "oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="], @@ -2087,6 +2096,8 @@ "oxlint": ["oxlint@1.71.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.71.0", "@oxlint/binding-android-arm64": "1.71.0", "@oxlint/binding-darwin-arm64": "1.71.0", "@oxlint/binding-darwin-x64": "1.71.0", "@oxlint/binding-freebsd-x64": "1.71.0", "@oxlint/binding-linux-arm-gnueabihf": "1.71.0", "@oxlint/binding-linux-arm-musleabihf": "1.71.0", "@oxlint/binding-linux-arm64-gnu": "1.71.0", "@oxlint/binding-linux-arm64-musl": "1.71.0", "@oxlint/binding-linux-ppc64-gnu": "1.71.0", "@oxlint/binding-linux-riscv64-gnu": "1.71.0", "@oxlint/binding-linux-riscv64-musl": "1.71.0", "@oxlint/binding-linux-s390x-gnu": "1.71.0", "@oxlint/binding-linux-x64-gnu": "1.71.0", "@oxlint/binding-linux-x64-musl": "1.71.0", "@oxlint/binding-openharmony-arm64": "1.71.0", "@oxlint/binding-win32-arm64-msvc": "1.71.0", "@oxlint/binding-win32-ia32-msvc": "1.71.0", "@oxlint/binding-win32-x64-msvc": "1.71.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-U1m1X+C0vDj7DC1e13IoZULzEcPczE7UOMTs8VlZGHUEIUaSTZKo5qkPsQEfzpgnQ29Pea/w3Xntk62UCecxZw=="], + "oxlint-plugin-react-doctor": ["oxlint-plugin-react-doctor@0.9.11", "", { "dependencies": { "@shaderfrog/glsl-parser": "^7.0.1", "@typescript-eslint/types": "^8.59.3", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "lightningcss": "^1.33.0", "oxc-parser": "^0.142.0" } }, "sha512-ZhW15wfFjQlUwAO6zG3jVZKse4/PBTCArhbibiidHmTlvOpPHPM1AjdYG13023pfsbbtVdy7Tb7KHfqbKt8rHg=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], @@ -2367,7 +2378,7 @@ "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], - "tailwindcss": ["tailwindcss@4.3.1", "", {}, "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q=="], + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], @@ -2573,11 +2584,7 @@ "@bundled-es-modules/deepmerge/deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], - - "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - - "@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], + "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -2603,10 +2610,6 @@ "@onyx-ai/shared/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], - "@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], - - "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], "@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], @@ -2667,7 +2670,7 @@ "@sentry/cli/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - "@tailwindcss/node/tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], @@ -2681,8 +2684,6 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tailwindcss/postcss/tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], - "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -2705,10 +2706,6 @@ "cosmiconfig/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], - "esquery/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -2733,10 +2730,6 @@ "minimizer-webpack-plugin/jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - - "next/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -2761,6 +2754,8 @@ "stacktrace-parser/type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], + "storybook/oxc-parser": ["oxc-parser@0.127.0", "", { "dependencies": { "@oxc-project/types": "^0.127.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.127.0", "@oxc-parser/binding-android-arm64": "0.127.0", "@oxc-parser/binding-darwin-arm64": "0.127.0", "@oxc-parser/binding-darwin-x64": "0.127.0", "@oxc-parser/binding-freebsd-x64": "0.127.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", "@oxc-parser/binding-linux-arm64-musl": "0.127.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-musl": "0.127.0", "@oxc-parser/binding-openharmony-arm64": "0.127.0", "@oxc-parser/binding-wasm32-wasi": "0.127.0", "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA=="], + "string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "style-dictionary/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -2785,14 +2780,12 @@ "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "@img/sharp-freebsd-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - - "@img/sharp-webcontainers-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -2813,8 +2806,6 @@ "@jsonjoy.com/fs-snapshot/@jsonjoy.com/util/@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q=="], - "@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ=="], "@radix-ui/react-menubar/@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="], @@ -2835,6 +2826,30 @@ "@sentry/cli/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "@unrs/resolver-binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "jest-config/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], "jest-config/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], @@ -2847,59 +2862,55 @@ "minimizer-webpack-plugin/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "next/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - - "next/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - - "next/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - "next/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "next/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + "path/util/inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="], - "next/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "next/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + "storybook/oxc-parser/@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.127.0", "", { "os": "android", "cpu": "arm" }, "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ=="], - "next/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + "storybook/oxc-parser/@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.127.0", "", { "os": "android", "cpu": "arm64" }, "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg=="], - "next/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + "storybook/oxc-parser/@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.127.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg=="], - "next/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + "storybook/oxc-parser/@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.127.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw=="], - "next/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + "storybook/oxc-parser/@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.127.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA=="], - "next/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + "storybook/oxc-parser/@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ=="], - "next/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + "storybook/oxc-parser/@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g=="], - "next/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + "storybook/oxc-parser/@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ=="], - "next/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + "storybook/oxc-parser/@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA=="], - "next/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + "storybook/oxc-parser/@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.127.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ=="], - "next/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + "storybook/oxc-parser/@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ=="], - "next/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + "storybook/oxc-parser/@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g=="], - "next/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + "storybook/oxc-parser/@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.127.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q=="], - "next/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + "storybook/oxc-parser/@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ=="], - "next/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + "storybook/oxc-parser/@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg=="], - "next/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + "storybook/oxc-parser/@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.127.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ=="], - "next/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "storybook/oxc-parser/@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.127.0", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ=="], - "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "storybook/oxc-parser/@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.127.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw=="], - "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "storybook/oxc-parser/@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.127.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw=="], - "path/util/inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="], + "storybook/oxc-parser/@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="], - "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "storybook/oxc-parser/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], "test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], @@ -2957,6 +2968,8 @@ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "@jest/reporters/glob/minimatch/brace-expansion": ["brace-expansion@2.1.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A=="], @@ -2973,6 +2986,10 @@ "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "storybook/oxc-parser/@oxc-parser/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + + "storybook/oxc-parser/@oxc-parser/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], @@ -2984,5 +3001,7 @@ "jest-runtime/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "storybook/oxc-parser/@oxc-parser/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], } } diff --git a/web/lib/opal/src/components/inputs/input-select/components.tsx b/web/lib/opal/src/components/inputs/input-select/components.tsx index b65914baf90..5dbaecca3ac 100644 --- a/web/lib/opal/src/components/inputs/input-select/components.tsx +++ b/web/lib/opal/src/components/inputs/input-select/components.tsx @@ -355,16 +355,17 @@ function InputSelectItem({ // registrations. const childrenRef = React.useRef(children); const iconRef = React.useRef(icon); - childrenRef.current = children; - iconRef.current = icon; + React.useLayoutEffect(() => { + childrenRef.current = children; + iconRef.current = icon; + }, [children, icon]); // Layout effect so the trigger never paints the placeholder on first // render when a value is already selected. Radix mounts closed Content // into a detached fragment, so this runs even while the menu is closed. - // Keyed on the rendered content (plain-text key, since RichStr identity - // churns per render) so the trigger mirror re-renders when the selected - // option's label or icon changes without a value change. - const childrenKey = toPlainString(children); + // Keyed on the raw rendered content so RichStr formatting changes still + // refresh the trigger without reacting to identity churn. + const childrenKey = typeof children === "string" ? children : children.raw; React.useLayoutEffect(() => { if (!isSelected) return; setSelectedItemDisplay({ childrenRef, iconRef }); diff --git a/web/lib/opal/src/components/modal/components.tsx b/web/lib/opal/src/components/modal/components.tsx index 9b423278d9a..6095da86e9b 100644 --- a/web/lib/opal/src/components/modal/components.tsx +++ b/web/lib/opal/src/components/modal/components.tsx @@ -147,27 +147,19 @@ function ModalContent({ hasUserTypedRef.current = true; }, []); - const containerNodeRef = React.useRef(null); - - const contentRef = React.useCallback( - (node: HTMLDivElement | null) => { - if (containerNodeRef.current) { - containerNodeRef.current.removeEventListener( - "input", - handleInput, - true - ); - } - if (node) { - node.addEventListener("input", handleInput, true); - containerNodeRef.current = node; - } else { - containerNodeRef.current = null; - } - }, - [handleInput] + const [contentNode, setContentNode] = React.useState( + null ); + React.useEffect(() => { + if (!contentNode) return; + + contentNode.addEventListener("input", handleInput, true); + return () => { + contentNode.removeEventListener("input", handleInput, true); + }; + }, [contentNode, handleInput]); + const handleInteractOutside = React.useCallback( (e: Event) => { if (!preventAccidentalClose) { @@ -198,9 +190,9 @@ function ModalContent({ } else if (ref) { ref.current = node; } - contentRef(node); + setContentNode(node); }, - [ref, contentRef] + [ref] ); // Center on [data-main-container] when present (the content area beside diff --git a/web/lib/opal/src/components/table/hooks/useDataTable.ts b/web/lib/opal/src/components/table/hooks/useDataTable.ts index 7aecfc6cdd5..ab7507d475a 100644 --- a/web/lib/opal/src/components/table/hooks/useDataTable.ts +++ b/web/lib/opal/src/components/table/hooks/useDataTable.ts @@ -270,7 +270,9 @@ export default function useDataTable( // Single ref for the whole serverSide config — prevents effects from // re-firing when the consumer passes an inline object each render. const serverSideRef = useRef(serverSide); - serverSideRef.current = serverSide; + useEffect(() => { + serverSideRef.current = serverSide; + }, [serverSide]); useEffect(() => { if (!isServerSide) return; @@ -423,7 +425,9 @@ export default function useDataTable( // ---- selection change callback ------------------------------------------ const isFirstRenderRef = useRef(true); const onSelectionChangeRef = useRef(onSelectionChange); - onSelectionChangeRef.current = onSelectionChange; + useEffect(() => { + onSelectionChangeRef.current = onSelectionChange; + }, [onSelectionChange]); useEffect(() => { if (isFirstRenderRef.current) { diff --git a/web/lib/opal/src/components/tabs/hooks.ts b/web/lib/opal/src/components/tabs/hooks.ts index 4c65fe3a6bc..58c72227542 100644 --- a/web/lib/opal/src/components/tabs/hooks.ts +++ b/web/lib/opal/src/components/tabs/hooks.ts @@ -46,6 +46,9 @@ export function usePillIndicator( const [isScrolling, setIsScrolling] = useState(false); const scrollTimeoutRef = useRef(null); + // The scroll-debounce timer is ref-held and cleared in this effect's + // cleanup. The rule cannot trace handler-created timers. + // oxlint-disable-next-line react-doctor/effect-needs-cleanup useEffect(() => { if (!enabled) return; diff --git a/web/lib/opal/src/layouts/root/components.tsx b/web/lib/opal/src/layouts/root/components.tsx index db98e33630a..30181353e11 100644 --- a/web/lib/opal/src/layouts/root/components.tsx +++ b/web/lib/opal/src/layouts/root/components.tsx @@ -46,7 +46,9 @@ export function SidebarStateProvider({ const [folded, setFoldedInternal] = useState(defaultFolded); const onFoldedChangeRef = useRef(onFoldedChange); - onFoldedChangeRef.current = onFoldedChange; + useEffect(() => { + onFoldedChangeRef.current = onFoldedChange; + }, [onFoldedChange]); const setFolded: Dispatch> = useCallback((value) => { setFoldedInternal((prev) => diff --git a/web/next.config.js b/web/next.config.js index 26552a56249..099eee85f92 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -7,8 +7,14 @@ const nextConfig = { productionBrowserSourceMaps: false, poweredByHeader: false, output: "standalone", + typescript: { + ignoreBuildErrors: process.env.SKIP_TYPE_CHECK === "1", + }, transpilePackages: ["@onyx-ai/opal", "@onyx-ai/shared"], typedRoutes: true, + // `next dev` otherwise appends its own managed block to web/AGENTS.md on every + // start, which dirties the tree. Keep our agent instructions author-owned. + agentRules: false, // NOTE: `reactCompiler` is set per-phase in module.exports below — enabled for // builds, disabled for the dev server. See the comment there for the rationale. // Pin the workspace root to this directory so Turbopack resolves modules diff --git a/web/package.json b/web/package.json index 9a3576bffb9..8223aecb0a2 100644 --- a/web/package.json +++ b/web/package.json @@ -12,6 +12,7 @@ "dev:clean": "bun run clean && next dev", "clean": "rm -rf .next node_modules/.cache *.tsbuildinfo", "build": "next build", + "build:fast": "SKIP_TYPE_CHECK=1 next build", "start": "next start", "lint": "oxlint", "lint:fix": "oxlint --fix", @@ -74,10 +75,10 @@ "linguist-languages": "^9.3.1", "lodash": "^4.18.1", "lowlight": "^3.3.0", - "lucide-react": "^1.16.0", + "lucide-react": "^1.25.0", "mime": "^4.1.0", "motion": "^12.42.2", - "next": "16.2.11", + "next": "16.3.0", "next-themes": "^0.4.4", "postcss": "^8.5.10", "posthog-js": "^1.376.3", @@ -136,9 +137,10 @@ "jest-environment-jsdom": "^30.2.0", "oxfmt": "0.59.0", "oxlint": "^1.66.0", + "oxlint-plugin-react-doctor": "0.9.11", "stats.js": "^0.17.0", "storybook": "10.5.0", - "tailwindcss": "^4.3.0", + "tailwindcss": "^4.3.3", "ts-unused-exports": "^11.0.1", "tsup": "^8.5.1", "typescript": "^5.9.3", diff --git a/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/StringPairListInput.tsx b/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/StringPairListInput.tsx index e3d6e4f348f..cd46fe83f07 100644 --- a/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/StringPairListInput.tsx +++ b/web/src/app/admin/connectors/[connector]/pages/ConnectorInput/StringPairListInput.tsx @@ -46,14 +46,32 @@ const StringPairListInput: React.FC = ({ // Stable per-row keys so removing a middle row doesn't shift native input // state (focus/autofill) onto the row that takes its index. Index keys would; // content-derived keys would remount the row on every keystroke. New rows are - // seeded here; the remove handler splices so each id stays with its row. - const rowIdsRef = React.useRef([]); - const nextRowIdRef = React.useRef(0); - while (rowIdsRef.current.length < pairs.length) { - rowIdsRef.current.push(nextRowIdRef.current++); - } - if (rowIdsRef.current.length > pairs.length) { - rowIdsRef.current.length = pairs.length; + // seeded here, and the remove handler drops the key at that index so each id + // stays with its row. + const [rowKeys, setRowKeys] = React.useState<{ + keys: number[]; + nextKey: number; + }>({ + keys: pairs.map((_, index) => index), + nextKey: pairs.length, + }); + if (rowKeys.keys.length < pairs.length) { + const keysToAdd = pairs.length - rowKeys.keys.length; + setRowKeys({ + keys: [ + ...rowKeys.keys, + ...Array.from( + { length: keysToAdd }, + (_, index) => rowKeys.nextKey + index + ), + ], + nextKey: rowKeys.nextKey + keysToAdd, + }); + } else if (rowKeys.keys.length > pairs.length) { + setRowKeys({ + keys: rowKeys.keys.slice(0, pairs.length), + nextKey: rowKeys.nextKey, + }); } return ( @@ -89,7 +107,7 @@ const StringPairListInput: React.FC = ({ {pairs.map((_, index) => (
= ({ type="button" tooltip="Remove" onClick={() => { - rowIdsRef.current.splice(index, 1); + setRowKeys((prev) => ({ + keys: prev.keys.filter((_, i) => i !== index), + nextKey: prev.nextKey, + })); arrayHelpers.remove(index); }} /> @@ -136,9 +157,9 @@ const StringPairListInput: React.FC = ({ icon={SvgPlusCircle} prominence="secondary" type="button" - onClick={() => - arrayHelpers.push({ [leftKey]: "", [rightKey]: "" }) - } + onClick={() => { + arrayHelpers.push({ [leftKey]: "", [rightKey]: "" }); + }} > Add New diff --git a/web/src/app/admin/token-rate-limits/page.tsx b/web/src/app/admin/token-rate-limits/page.tsx deleted file mode 100644 index 6afd84e2f67..00000000000 --- a/web/src/app/admin/token-rate-limits/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { redirect } from "next/navigation"; -import type { Route } from "next"; -import { EE_ENABLED } from "@/lib/constants"; -import { ADMIN_ROUTES } from "@/lib/admin-routes"; -import { SettingsLayouts } from "@opal/layouts"; -import TokenRateLimitsPanel from "./TokenRateLimitsPanel"; - -export default function Page() { - if (EE_ENABLED) { - redirect(ADMIN_ROUTES.USAGE.path as Route); - } - - return ( - - - - - - - ); -} diff --git a/web/src/app/app/message/HumanMessage.tsx b/web/src/app/app/message/HumanMessage.tsx index 90aa49e8804..272bf735e72 100644 --- a/web/src/app/app/message/HumanMessage.tsx +++ b/web/src/app/app/message/HumanMessage.tsx @@ -56,8 +56,6 @@ function MessageEditing({ className={cn( "w-full h-full resize-none outline-hidden bg-transparent overflow-y-scroll whitespace-normal break-word" )} - aria-multiline - role="textarea" value={editedContent} style={{ scrollbarWidth: "thin" }} onChange={(e) => { diff --git a/web/src/app/app/message/MultiModelResponseView.tsx b/web/src/app/app/message/MultiModelResponseView.tsx index fe430a4bd5a..0979b3ea085 100644 --- a/web/src/app/app/message/MultiModelResponseView.tsx +++ b/web/src/app/app/message/MultiModelResponseView.tsx @@ -102,28 +102,20 @@ export default function MultiModelResponseView({ const [selectionExiting, setSelectionExiting] = useState(false); // Measures the overflow-hidden carousel container for responsive preferred-panel sizing. const [trackContainerW, setTrackContainerW] = useState(0); - const roRef = useRef(null); const trackContainerElRef = useRef(null); + const [trackContainerEl, setTrackContainerEl] = + useState(null); const trackContainerRef = useCallback((el: HTMLDivElement | null) => { trackContainerElRef.current = el; - if (roRef.current) { - roRef.current.disconnect(); - roRef.current = null; - } - if (!el) return; - const ro = new ResizeObserver(([entry]) => { - setTrackContainerW(entry?.contentRect.width ?? 0); - }); - ro.observe(el); - setTrackContainerW(el.offsetWidth); - roRef.current = ro; + setTrackContainerEl(el); }, []); // Measures the preferred panel's height to cap non-preferred panels in selection mode. const [preferredPanelHeight, setPreferredPanelHeight] = useState< number | null >(null); - const preferredRoRef = useRef(null); + const [preferredPanelEl, setPreferredPanelEl] = + useState(null); // Refs to each panel wrapper for height animation on deselect const panelElsRef = useRef>(new Map()); @@ -148,21 +140,36 @@ export default function MultiModelResponseView({ }); }, [preferredPanelHeight, preferredIndex, hiddenPanels, responses]); - const preferredPanelRef = useCallback((el: HTMLDivElement | null) => { - if (preferredRoRef.current) { - preferredRoRef.current.disconnect(); - preferredRoRef.current = null; - } - if (!el) { + useLayoutEffect(() => { + if (!trackContainerEl) return; + const ro = new ResizeObserver(([entry]) => { + setTrackContainerW(entry?.contentRect.width ?? 0); + }); + ro.observe(trackContainerEl); + setTrackContainerW(trackContainerEl.offsetWidth); + return () => ro.disconnect(); + }, [trackContainerEl]); + + useLayoutEffect(() => { + if (!preferredPanelEl) { setPreferredPanelHeight(null); return; } const ro = new ResizeObserver(([entry]) => { setPreferredPanelHeight(entry?.contentRect.height ?? 0); }); - ro.observe(el); - setPreferredPanelHeight(el.offsetHeight); - preferredRoRef.current = ro; + ro.observe(preferredPanelEl); + setPreferredPanelHeight(preferredPanelEl.offsetHeight); + return () => ro.disconnect(); + }, [preferredPanelEl]); + + useEffect(() => { + return () => { + if (deselectTimeoutRef.current !== null) { + clearTimeout(deselectTimeoutRef.current); + deselectTimeoutRef.current = null; + } + }; }, []); const isGenerating = useMemo( @@ -554,7 +561,7 @@ export default function MultiModelResponseView({ } else { panelElsRef.current.delete(r.modelIndex); } - if (isPref) preferredPanelRef(el); + if (isPref) setPreferredPanelEl(el); }} style={{ width: `${selectionEntered ? finalW : startW}px`, diff --git a/web/src/app/app/message/messageComponents/hooks/useAuthErrors.ts b/web/src/app/app/message/messageComponents/hooks/useAuthErrors.ts index ff8a4a89ccf..29a67406b70 100644 --- a/web/src/app/app/message/messageComponents/hooks/useAuthErrors.ts +++ b/web/src/app/app/message/messageComponents/hooks/useAuthErrors.ts @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import { useMemo } from "react"; import { CustomToolDelta, Packet, @@ -11,43 +11,37 @@ interface AuthError { } export function useAuthErrors(rawPackets: Packet[]): AuthError[] { - const stateRef = useRef<{ processedCount: number; errors: AuthError[] }>({ - processedCount: 0, - errors: [], - }); - - // Reset if packets shrunk (e.g. new message) - if (rawPackets.length < stateRef.current.processedCount) { - stateRef.current = { processedCount: 0, errors: [] }; - } + // Keyed on the packet array so re-renders between packet batches reuse + // the same result identity instead of rescanning. + return useMemo(() => computeAuthErrors(rawPackets), [rawPackets]); +} + +function computeAuthErrors(rawPackets: Packet[]): AuthError[] { + const errors: AuthError[] = []; + + for (const packet of rawPackets) { + if (packet.obj.type !== PacketType.CUSTOM_TOOL_DELTA) { + continue; + } + + const delta = packet.obj as CustomToolDelta; + if (!delta.error?.is_auth_error) { + continue; + } + + const alreadyPresent = errors.some( + (error) => + (delta.tool_id != null && error.toolId === delta.tool_id) || + (delta.tool_id == null && error.toolName === delta.tool_name) + ); - // Process only new packets (incremental, like usePacketProcessor) - if (rawPackets.length > stateRef.current.processedCount) { - let newErrors = stateRef.current.errors; - for (let i = stateRef.current.processedCount; i < rawPackets.length; i++) { - const packet = rawPackets[i]!; - if (packet.obj.type === PacketType.CUSTOM_TOOL_DELTA) { - const delta = packet.obj as CustomToolDelta; - if (delta.error?.is_auth_error) { - const alreadyPresent = newErrors.some( - (e) => - (delta.tool_id != null && e.toolId === delta.tool_id) || - (delta.tool_id == null && e.toolName === delta.tool_name) - ); - if (!alreadyPresent) { - newErrors = [ - ...newErrors, - { toolName: delta.tool_name, toolId: delta.tool_id ?? null }, - ]; - } - } - } + if (!alreadyPresent) { + errors.push({ + toolName: delta.tool_name, + toolId: delta.tool_id ?? null, + }); } - stateRef.current = { - processedCount: rawPackets.length, - errors: newErrors, - }; } - return stateRef.current.errors; + return errors; } diff --git a/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx b/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx index ac99ec60f17..a425d0fa4a0 100644 --- a/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx +++ b/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx @@ -339,8 +339,10 @@ export const MessageTextRenderer: MessageRenderer< // never change — otherwise every typewriter tick would invalidate // React reconciliation on the markdown subtree. const stateRef = useRef(state); + // oxlint-disable-next-line react-doctor/no-ref-current-in-render -- render-phase mirror keeps markdownComponents identities stable (see block comment above) stateRef.current = state; const processedContentRef = useRef(processedContent); + // oxlint-disable-next-line react-doctor/no-ref-current-in-render -- render-phase mirror keeps markdownComponents identities stable (see block comment above) processedContentRef.current = processedContent; const markdownComponents = useMemo( diff --git a/web/src/app/app/message/messageComponents/timeline/hooks/usePacedTurnGroups.ts b/web/src/app/app/message/messageComponents/timeline/hooks/usePacedTurnGroups.ts index 49d57b85711..a31d0bfe915 100644 --- a/web/src/app/app/message/messageComponents/timeline/hooks/usePacedTurnGroups.ts +++ b/web/src/app/app/message/messageComponents/timeline/hooks/usePacedTurnGroups.ts @@ -59,7 +59,7 @@ interface PacingState { nodeId: string | null; } -function createInitialPacingState(): PacingState { +function createInitialPacingState(nodeId: string): PacingState { return { revealedStepKeys: new Set(), lastRevealedPacketType: null, @@ -67,7 +67,7 @@ function createInitialPacingState(): PacingState { pacingTimer: null, toolPacingComplete: false, stopPacketSeen: false, - nodeId: null, + nodeId, }; } @@ -97,8 +97,11 @@ export function usePacedTurnGroups( pacedDisplayGroups: GroupedPacket[]; pacedFinalAnswerComing: boolean; } { + // Stable nodeId string for comparison + const nodeIdStr = String(nodeId); + // Ref-based pacing state (no re-renders) - const stateRef = useRef(createInitialPacingState()); + const stateRef = useRef(createInitialPacingState(nodeIdStr)); // Track previous finalAnswerComing to detect tool-after-message transitions const prevFinalAnswerComingRef = useRef(finalAnswerComing); @@ -110,21 +113,13 @@ export function usePacedTurnGroups( // Trigger re-render when content should update // Used in useMemo dependencies since state.revealedStepKeys is stored in a ref const [revealTrigger, setRevealTrigger] = useState(0); - - // Stable nodeId string for comparison - const nodeIdStr = String(nodeId); - - // Reset on nodeId change - if (stateRef.current.nodeId !== nodeIdStr) { - if (stateRef.current.pacingTimer) { - clearTimeout(stateRef.current.pacingTimer); - } - stateRef.current = createInitialPacingState(); - stateRef.current.nodeId = nodeIdStr; - prevPacedRef.current = []; - } - - const state = stateRef.current; + const [timerTrigger, setTimerTrigger] = useState(0); + const resetState = useMemo( + () => createInitialPacingState(nodeIdStr), + [nodeIdStr] + ); + const hasNodeChanged = stateRef.current.nodeId !== nodeIdStr; + const state = hasNodeChanged ? resetState : stateRef.current; // Bypass pacing for completed messages (old messages loaded from history) // If stopPacketSeen is true on first render, return everything immediately @@ -145,7 +140,7 @@ export function usePacedTurnGroups( // Schedule next step if more pending (always delay, regardless of type) if (state.pendingSteps.length > 0) { - state.pacingTimer = setTimeout(revealNextPendingStep, PACING_DELAY_MS); + setTimerTrigger((t) => t + 1); setRevealTrigger((t) => t + 1); return; } @@ -157,6 +152,48 @@ export function usePacedTurnGroups( setRevealTrigger((t) => t + 1); }, []); + useEffect(() => { + if (!hasNodeChanged) { + return; + } + + if (stateRef.current.pacingTimer) { + clearTimeout(stateRef.current.pacingTimer); + } + + stateRef.current = resetState; + prevPacedRef.current = []; + prevFinalAnswerComingRef.current = finalAnswerComing; + }, [finalAnswerComing, hasNodeChanged, resetState]); + + useEffect(() => { + const state = stateRef.current; + + if ( + shouldBypassPacing || + state.pendingSteps.length === 0 || + state.pacingTimer + ) { + return; + } + + const timer = setTimeout(() => { + if (stateRef.current.pacingTimer === timer) { + stateRef.current.pacingTimer = null; + } + revealNextPendingStep(); + }, PACING_DELAY_MS); + + state.pacingTimer = timer; + + return () => { + clearTimeout(timer); + if (stateRef.current.pacingTimer === timer) { + stateRef.current.pacingTimer = null; + } + }; + }, [revealNextPendingStep, shouldBypassPacing, timerTrigger]); + // Process incoming turn groups useEffect(() => { // Skip processing when bypassing pacing @@ -256,7 +293,7 @@ export function usePacedTurnGroups( // Start timer if not already running if (!state.pacingTimer && state.pendingSteps.length === 1) { - state.pacingTimer = setTimeout(revealNextPendingStep, PACING_DELAY_MS); + setTimerTrigger((t) => t + 1); } } @@ -270,17 +307,11 @@ export function usePacedTurnGroups( finalAnswerComing, revealNextPendingStep, shouldBypassPacing, + // Re-process after the node-change reset so the fresh state's first step + // reveals immediately even when the group identities are unchanged. + nodeIdStr, ]); - // Cleanup timer on unmount - useEffect(() => { - return () => { - if (stateRef.current.pacingTimer) { - clearTimeout(stateRef.current.pacingTimer); - } - }; - }, []); - // Build paced turn groups from revealed step keys // Memoized to prevent unnecessary re-renders in downstream components // revealTrigger is included because state.revealedStepKeys is stored in a ref @@ -305,7 +336,7 @@ export function usePacedTurnGroups( // Stabilize: reuse previous TurnGroup objects when their content hasn't changed. // This preserves referential equality for completed groups, preventing // unnecessary re-renders in downstream components (e.g. SearchChipList). - const prev = prevPacedRef.current; + const prev = hasNodeChanged ? [] : prevPacedRef.current; if (prev.length === result.length) { let allMatch = true; for (let i = 0; i < result.length; i++) { @@ -332,10 +363,13 @@ export function usePacedTurnGroups( } } - prevPacedRef.current = result; return result; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [toolTurnGroups, revealTrigger, shouldBypassPacing]); + }, [toolTurnGroups, revealTrigger, shouldBypassPacing, hasNodeChanged]); + + useEffect(() => { + prevPacedRef.current = pacedTurnGroups; + }, [pacedTurnGroups]); // Only return display groups when tool pacing is complete (or bypassing). // Also bypass when stop packet is already seen (e.g. history reload of stopped messages) diff --git a/web/src/app/app/message/messageComponents/timeline/hooks/usePacketProcessor.ts b/web/src/app/app/message/messageComponents/timeline/hooks/usePacketProcessor.ts index ba7f099f693..27338acde64 100644 --- a/web/src/app/app/message/messageComponents/timeline/hooks/usePacketProcessor.ts +++ b/web/src/app/app/message/messageComponents/timeline/hooks/usePacketProcessor.ts @@ -1,3 +1,6 @@ +/* oxlint-disable react-doctor/no-ref-current-in-render -- render-phase + incremental processing is the core design: the packet cursor makes + replays idempotent and consumers need the state in the same commit. */ import { useRef, useState, useMemo, useCallback } from "react"; import { Packet, diff --git a/web/src/app/app/message/messageComponents/timeline/renderers/reasoning/ReasoningRenderer.tsx b/web/src/app/app/message/messageComponents/timeline/renderers/reasoning/ReasoningRenderer.tsx index 4d6825797fc..f4a0d68be28 100644 --- a/web/src/app/app/message/messageComponents/timeline/renderers/reasoning/ReasoningRenderer.tsx +++ b/web/src/app/app/message/messageComponents/timeline/renderers/reasoning/ReasoningRenderer.tsx @@ -111,7 +111,6 @@ export const ReasoningRenderer: MessageRenderer< const [reasoningStartTime, setReasoningStartTime] = useState( null ); - const timeoutRef = useRef(null); const completionHandledRef = useRef(false); // Track when reasoning starts @@ -124,35 +123,32 @@ export const ReasoningRenderer: MessageRenderer< // Handle reasoning completion with minimum duration useEffect(() => { if ( - hasEnd && - reasoningStartTime !== null && - !completionHandledRef.current + !hasEnd || + reasoningStartTime === null || + completionHandledRef.current ) { - completionHandledRef.current = true; - const elapsedTime = Date.now() - reasoningStartTime; - const minimumThinkingDuration = animate ? THINKING_MIN_DURATION_MS : 0; + return; + } - if (elapsedTime >= minimumThinkingDuration) { - // Enough time has passed, complete immediately + const complete = () => { + if (!completionHandledRef.current) { + completionHandledRef.current = true; onComplete(); - } else { - // Not enough time has passed, delay completion - const remainingTime = minimumThinkingDuration - elapsedTime; - timeoutRef.current = setTimeout(() => { - onComplete(); - }, remainingTime); } + }; + + const elapsedTime = Date.now() - reasoningStartTime; + const minimumThinkingDuration = animate ? THINKING_MIN_DURATION_MS : 0; + + if (elapsedTime >= minimumThinkingDuration) { + complete(); + return; } - }, [hasEnd, reasoningStartTime, animate, onComplete]); - // Cleanup timeout on unmount - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); + const remainingTime = minimumThinkingDuration - elapsedTime; + const timeout = setTimeout(complete, remainingTime); + return () => clearTimeout(timeout); + }, [hasEnd, reasoningStartTime, animate, onComplete]); // Markdown renderer callback for ExpandableTextDisplay // Uses collapsed components (no spacing) in collapsed view, normal spacing in expanded modal diff --git a/web/src/app/craft/components/BuildMessageList.tsx b/web/src/app/craft/components/BuildMessageList.tsx index d82581f970f..ccc74b1ba9d 100644 --- a/web/src/app/craft/components/BuildMessageList.tsx +++ b/web/src/app/craft/components/BuildMessageList.tsx @@ -213,7 +213,9 @@ export default function BuildMessageList({ initial={ opts.isCurrentStream ? { opacity: 0, y: -4, height: 0 } : false } + // oxlint-disable-next-line react-doctor/no-layout-property-animation -- height 0/auto must reflow the message list, transform cannot animate={{ opacity: 1, y: 0, height: "auto" }} + // oxlint-disable-next-line react-doctor/no-layout-property-animation -- height/marginTop collapse must reflow the message list, transform cannot exit={{ opacity: 0, y: -6, height: 0, marginTop: 0 }} transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }} > diff --git a/web/src/app/craft/components/ChatPanel.tsx b/web/src/app/craft/components/ChatPanel.tsx index 8ea17c37674..155d70f2145 100644 --- a/web/src/app/craft/components/ChatPanel.tsx +++ b/web/src/app/craft/components/ChatPanel.tsx @@ -206,6 +206,9 @@ export default function BuildChatPanel({ turnId: string; timer: ReturnType; } | null>(null); + const nameSessionTimeoutRef = useRef | null>( + null + ); const isPreProvisioning = useIsPreProvisioning(); const isPreProvisioningFailed = useIsPreProvisioningFailed(); const preProvisionedSessionId = usePreProvisionedSessionId(); @@ -536,7 +539,16 @@ export default function BuildChatPanel({ // Schedule naming after delay (message will be saved by then) // Note: Don't call refreshSessionHistory() here - it would overwrite the // optimistic update from consumePreProvisionedSession() before the message is saved - setTimeout(() => nameBuildSession(newSessionId), 1000); + if (nameSessionTimeoutRef.current !== null) { + clearTimeout(nameSessionTimeoutRef.current); + } + // Session naming is a store action that must survive unmount. Firing + // before the 1s save window would name against an unsaved message. + // oxlint-disable-next-line react-doctor/effect-needs-cleanup + nameSessionTimeoutRef.current = setTimeout(() => { + nameSessionTimeoutRef.current = null; + nameBuildSession(newSessionId); + }, 1000); // Stream the response (uses session ID directly, not currentSessionId) await streamMessage(newSessionId, message, chosen, attachments); diff --git a/web/src/app/craft/components/OpencodeDebugLogs.tsx b/web/src/app/craft/components/OpencodeDebugLogs.tsx index f5b954560e4..1c717ac259d 100644 --- a/web/src/app/craft/components/OpencodeDebugLogs.tsx +++ b/web/src/app/craft/components/OpencodeDebugLogs.tsx @@ -115,7 +115,9 @@ function LogStreamPane({ open }: LogStreamPaneProps) { // Refs mirror the state inside the SSE reader's hot loop without // forcing it to re-bind on every state change. const followRef = useRef(follow); - followRef.current = follow; + useEffect(() => { + followRef.current = follow; + }, [follow]); const appendLine = useCallback((text: string) => { const line: LogLine = { diff --git a/web/src/app/craft/components/SkillsStaleNotice.test.tsx b/web/src/app/craft/components/SkillsStaleNotice.test.tsx index f061ca1ee1d..85872df7abf 100644 --- a/web/src/app/craft/components/SkillsStaleNotice.test.tsx +++ b/web/src/app/craft/components/SkillsStaleNotice.test.tsx @@ -36,9 +36,17 @@ describe("SkillsStaleNotice", () => { }); }); - it("disables reload while a turn is active", () => { - render(); + it("reveals retained stale state after an active turn stops", () => { + const { rerender } = render( + + ); - expect(screen.getByRole("button", { name: "Reload" })).toBeDisabled(); + expect( + screen.queryByRole("button", { name: "Reload" }) + ).not.toBeInTheDocument(); + + rerender(); + + expect(screen.getByRole("button", { name: "Reload" })).toBeEnabled(); }); }); diff --git a/web/src/app/craft/components/SkillsStaleNotice.tsx b/web/src/app/craft/components/SkillsStaleNotice.tsx index 3b22a21f32b..ecfdc6dabc6 100644 --- a/web/src/app/craft/components/SkillsStaleNotice.tsx +++ b/web/src/app/craft/components/SkillsStaleNotice.tsx @@ -34,20 +34,15 @@ export default function SkillsStaleNotice({ } }; + if (turnActive) return null; + return ( + } diff --git a/web/src/app/craft/contexts/BuildContext.tsx b/web/src/app/craft/contexts/BuildContext.tsx index 6e05fdfc0ce..eb9bbfa53f1 100644 --- a/web/src/app/craft/contexts/BuildContext.tsx +++ b/web/src/app/craft/contexts/BuildContext.tsx @@ -42,12 +42,10 @@ export function BuildProvider({ children }: BuildProviderProps) { }, []); const toggleVideoBackground = useCallback(() => { - setVideoBackgroundEnabled((prev) => { - const next = !prev; - localStorage.setItem(VIDEO_BACKGROUND_STORAGE_KEY, String(next)); - return next; - }); - }, []); + const next = !videoBackgroundEnabled; + setVideoBackgroundEnabled(next); + localStorage.setItem(VIDEO_BACKGROUND_STORAGE_KEY, String(next)); + }, [videoBackgroundEnabled]); const value = useMemo( () => ({ diff --git a/web/src/app/craft/contexts/UploadFilesContext.tsx b/web/src/app/craft/contexts/UploadFilesContext.tsx index 79672acb015..192a317b962 100644 --- a/web/src/app/craft/contexts/UploadFilesContext.tsx +++ b/web/src/app/craft/contexts/UploadFilesContext.tsx @@ -270,6 +270,7 @@ export function UploadFilesProvider({ children }: UploadFilesProviderProps) { const [currentMessageFiles, setCurrentMessageFiles] = useState( [] ); + const currentMessageFilesRef = useRef([]); const [activeSessionId, setActiveSessionId] = useState(null); // Get triggerFilesRefresh from the store to refresh the file explorer @@ -305,6 +306,10 @@ export function UploadFilesProvider({ children }: UploadFilesProviderProps) { ); }, [currentMessageFiles]); + useEffect(() => { + currentMessageFilesRef.current = currentMessageFiles; + }, [currentMessageFiles]); + // ========================================================================= // Internal operations (not exposed to consumers) // ========================================================================= @@ -318,26 +323,23 @@ export function UploadFilesProvider({ children }: UploadFilesProviderProps) { async (sessionId: string): Promise => { if (isUploadingPendingRef.current) return; - // Read current files and find pending ones atomically - let pendingFiles: BuildFile[] = []; - setCurrentMessageFiles((prev) => { - pendingFiles = prev.filter( - (f) => f.status === UploadFileStatus.PENDING && f.file - ); - // Mark as uploading in the same state update to avoid race conditions - if (pendingFiles.length > 0) { - return prev.map((f) => - pendingFiles.some((pf) => pf.id === f.id) - ? { ...f, status: UploadFileStatus.UPLOADING } - : f - ); - } - return prev; - }); + const currentFiles = currentMessageFilesRef.current; + const pendingFiles = currentFiles.filter( + (f) => f.status === UploadFileStatus.PENDING && f.file + ); if (pendingFiles.length === 0) return; isUploadingPendingRef.current = true; + // Functional update so a concurrent add/remove in the same batch is + // never clobbered. The ref syncs from state in its own effect. + setCurrentMessageFiles((prev) => + prev.map((f) => + pendingFiles.some((pf) => pf.id === f.id) + ? { ...f, status: UploadFileStatus.UPLOADING } + : f + ) + ); try { // Upload in parallel @@ -699,23 +701,22 @@ export function UploadFilesProvider({ children }: UploadFilesProviderProps) { // Track this deletion to prevent refetch race condition activeDeletionsRef.current.add(fileId); - // Use functional update to get current state and avoid stale closures - let removedFile: BuildFile | null = null; - let removedIndex = -1; - - setCurrentMessageFiles((prev) => { - const index = prev.findIndex((f) => f.id === fileId); - if (index === -1) return prev; + const currentFiles = currentMessageFilesRef.current; + const removedIndex = currentFiles.findIndex((f) => f.id === fileId); + if (removedIndex === -1) { + activeDeletionsRef.current.delete(fileId); + return; + } - // Capture file info for potential rollback and backend deletion - const file = prev[index]; - if (!file) return prev; - removedFile = file; - removedIndex = index; + const removedFile = currentFiles[removedIndex]; + if (!removedFile) { + activeDeletionsRef.current.delete(fileId); + return; + } - // Return filtered array (optimistic removal) - return prev.filter((f) => f.id !== fileId); - }); + // Functional update keeps concurrent same-batch changes. The ref syncs + // from state in its own effect. + setCurrentMessageFiles((prev) => prev.filter((f) => f.id !== fileId)); // After state update, trigger backend deletion if needed // Use setTimeout to ensure state update has completed diff --git a/web/src/app/craft/hooks/loadSessionRestore.test.ts b/web/src/app/craft/hooks/loadSessionRestore.test.ts index c1f5c21a133..8e4092f6c7f 100644 --- a/web/src/app/craft/hooks/loadSessionRestore.test.ts +++ b/web/src/app/craft/hooks/loadSessionRestore.test.ts @@ -12,10 +12,11 @@ const SESSION_ID = "11111111-1111-1111-1111-111111111111"; // Minimal DetailedSessionResponse shapes — loadSession only reads status, // session_loaded_in_sandbox, nextjs_port, and sandbox.status. -function sleepingSession(): unknown { +function sleepingSession(): Record { return { id: SESSION_ID, status: "idle", + skills_stale: false, nextjs_port: null, session_loaded_in_sandbox: false, sandbox: { id: "sb1", status: "sleeping" }, @@ -28,6 +29,7 @@ function runningSession( return { id: SESSION_ID, status: "active", + skills_stale: false, nextjs_port: nextjsPort, session_loaded_in_sandbox: true, sandbox: { id: "sb1", status: "running" }, @@ -38,6 +40,14 @@ function webappInfo(has_webapp: boolean | null, ready: boolean): unknown { return { has_webapp, webapp_url: null, status: "running", ready }; } +function deferred(): { promise: Promise; resolve: (value: T) => void } { + let resolve: (value: T) => void = () => {}; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + describe("loadSession restore status", () => { beforeEach(() => { jest.clearAllMocks(); @@ -88,6 +98,23 @@ describe("loadSession restore status", () => { expect(session?.sandbox?.status).toBe("failed"); }); + it("clears stale skills after a successful session restore", async () => { + mockedApi.fetchSession.mockResolvedValue({ + ...sleepingSession(), + skills_stale: true, + } as never); + mockedApi.restoreSession.mockResolvedValue({ + ...runningSession(), + skills_stale: false, + } as never); + + await useBuildSessionStore.getState().loadSession(SESSION_ID); + + expect( + useBuildSessionStore.getState().sessions.get(SESSION_ID)?.skillsStale + ).toBe(false); + }); + it("waits for the webapp before flipping to running, then shows running", async () => { mockedApi.fetchSession.mockResolvedValue(sleepingSession() as never); mockedApi.restoreSession.mockResolvedValue(runningSession() as never); @@ -471,7 +498,7 @@ describe("loadSession restore status", () => { expect(session?.activeTurnLocalOwner).toBe(false); }); - it("preserves stale-skill state while loading a pre-provisioned turn", async () => { + it("retains fetched stale-skill state during a pre-provisioned turn", async () => { mockedApi.fetchSession.mockResolvedValue({ ...runningSession(), skills_stale: true, @@ -494,6 +521,31 @@ describe("loadSession restore status", () => { .getState() .loadSession(SESSION_ID, { force: true }); + expect( + useBuildSessionStore.getState().sessions.get(SESSION_ID)?.skillsStale + ).toBe(true); + }); + + it("rejects a load fetched before a newer stale-skill update", async () => { + const messages = deferred(); + mockedApi.fetchSession.mockResolvedValue({ + ...runningSession(), + skills_stale: true, + } as never); + mockedApi.fetchMessages.mockReturnValue(messages.promise as never); + + const load = useBuildSessionStore + .getState() + .loadSession(SESSION_ID, { force: true }); + await Promise.resolve(); + expect(mockedApi.fetchMessages).toHaveBeenCalled(); + + useBuildSessionStore + .getState() + .updateSessionData(SESSION_ID, { skillsStale: false }); + messages.resolve([]); + await load; + expect( useBuildSessionStore.getState().sessions.get(SESSION_ID)?.skillsStale ).toBe(false); diff --git a/web/src/app/craft/hooks/useBuildSessionController.test.tsx b/web/src/app/craft/hooks/useBuildSessionController.test.tsx index bcc07d6212d..364361d3177 100644 --- a/web/src/app/craft/hooks/useBuildSessionController.test.tsx +++ b/web/src/app/craft/hooks/useBuildSessionController.test.tsx @@ -41,7 +41,7 @@ describe("useBuildSessionController", () => { useBuildSessionStore.getState().setCurrentSession(SESSION_ID); }); - it("refreshes stale skill state on mount and browser focus", async () => { + it("marks skills stale from reads without clearing confirmed stale state", async () => { jest.mocked(api.fetchSession).mockResolvedValue({ skills_stale: true, } as never); @@ -65,10 +65,12 @@ describe("useBuildSessionController", () => { act(() => window.dispatchEvent(new Event("focus"))); await waitFor(() => { - expect( - useBuildSessionStore.getState().sessions.get(SESSION_ID)?.skillsStale - ).toBe(false); + expect(api.fetchSession).toHaveBeenCalledTimes(2); }); + await act(async () => Promise.resolve()); + expect( + useBuildSessionStore.getState().sessions.get(SESSION_ID)?.skillsStale + ).toBe(true); }); it("does not restore stale state after an intervening reload", async () => { diff --git a/web/src/app/craft/hooks/useBuildSessionController.ts b/web/src/app/craft/hooks/useBuildSessionController.ts index 5df7d7992b8..d33b57ca6d1 100644 --- a/web/src/app/craft/hooks/useBuildSessionController.ts +++ b/web/src/app/craft/hooks/useBuildSessionController.ts @@ -109,12 +109,13 @@ export function useBuildSessionController({ const session = await fetchSession(sessionId, { checkWorkspace: false, }); + if (!session.skills_stale) return; const currentSession = useBuildSessionStore .getState() .sessions.get(sessionId); if (currentSession?.skillsStaleRevision !== skillsStaleRevision) return; updateSessionData(sessionId, { - skillsStale: session.skills_stale, + skillsStale: true, }); } catch { // Keep the usable cached session on transient refresh failures. diff --git a/web/src/app/craft/hooks/useBuildSessionStore.ts b/web/src/app/craft/hooks/useBuildSessionStore.ts index 468062ea779..147e77a174d 100644 --- a/web/src/app/craft/hooks/useBuildSessionStore.ts +++ b/web/src/app/craft/hooks/useBuildSessionStore.ts @@ -1506,6 +1506,11 @@ export const useBuildSessionStore = create()((set, get) => ({ // Set as current and mark as loading setCurrentSession(sessionId); + const skillsStaleRevision = + get().sessions.get(sessionId)!.skillsStaleRevision; + const canApplySkillsStale = () => + get().sessions.get(sessionId)?.skillsStaleRevision === + skillsStaleRevision; try { // First fetch session to check sandbox status @@ -1611,9 +1616,8 @@ export const useBuildSessionStore = create()((set, get) => ({ sandbox, agentProvider: sessionData.agent_provider, agentModel: sessionData.agent_model, - // Persisted loads reconcile stale state. Optimistic welcome loads keep - // their live local state until the turn settles. - ...(useDbMessages && { skillsStale: sessionData.skills_stale }), + ...(sessionData.skills_stale && + canApplySkillsStale() && { skillsStale: true }), origin: sessionData.origin, activeTurnId: resolvedActiveTurnId, activeTurnIndex: resolvedActiveTurnIndex, @@ -1628,6 +1632,8 @@ export const useBuildSessionStore = create()((set, get) => ({ }); if (needsRestore) { + const skillsStaleRevisionBeforeRestore = + get().sessions.get(sessionId)?.skillsStaleRevision; try { sessionData = await restoreSession(sessionId); } catch (restoreErr) { @@ -1649,7 +1655,10 @@ export const useBuildSessionStore = create()((set, get) => ({ sandbox: sessionData.sandbox ? { ...sessionData.sandbox, status: "restoring" } : sessionData.sandbox, - skillsStale: sessionData.skills_stale, + ...(get().sessions.get(sessionId)?.skillsStaleRevision === + skillsStaleRevisionBeforeRestore && { + skillsStale: sessionData.skills_stale, + }), webappNeedsRefresh: (get().sessions.get(sessionId)?.webappNeedsRefresh || 0) + 1, }); diff --git a/web/src/app/craft/hooks/useBuildStreaming.test.tsx b/web/src/app/craft/hooks/useBuildStreaming.test.tsx index 5d3c1486879..a10ee7fbf57 100644 --- a/web/src/app/craft/hooks/useBuildStreaming.test.tsx +++ b/web/src/app/craft/hooks/useBuildStreaming.test.tsx @@ -119,6 +119,45 @@ describe("useBuildStreaming thinking packets", () => { }); }); + it.each([ + { changesDuringTurn: false, expected: false }, + { changesDuringTurn: true, expected: true }, + ])( + "reconciles stale skills after turn creation when changesDuringTurn is $changesDuringTurn", + async ({ changesDuringTurn, expected }) => { + let resolveTurn: ( + turn: Awaited> + ) => void = () => {}; + jest.mocked(createTurn).mockReturnValueOnce( + new Promise((resolve) => { + resolveTurn = resolve; + }) + ); + useBuildSessionStore.getState().updateSessionData(sessionId, { + skillsStale: true, + }); + const { result } = renderHook(() => useBuildStreaming()); + + const stream = result.current.streamMessage(sessionId, "build the app"); + if (changesDuringTurn) { + useBuildSessionStore.getState().updateSessionData(sessionId, { + skillsStale: true, + }); + } + resolveTurn({ + session_id: sessionId, + turn_id: "turn-thinking", + status: "QUEUED", + turn_index: 0, + }); + await act(async () => stream); + + expect( + useBuildSessionStore.getState().sessions.get(sessionId)?.skillsStale + ).toBe(expected); + } + ); + it("does not reset the abort controller when a newer turn took ownership mid-stream", async () => { const newerController = new AbortController(); jest.mocked(processSSEStream).mockImplementationOnce(async () => { diff --git a/web/src/app/craft/hooks/useBuildStreaming.ts b/web/src/app/craft/hooks/useBuildStreaming.ts index a379d6d2a0a..53c86451469 100644 --- a/web/src/app/craft/hooks/useBuildStreaming.ts +++ b/web/src/app/craft/hooks/useBuildStreaming.ts @@ -957,6 +957,7 @@ export function useBuildStreaming() { ): Promise => { const currentState = useBuildSessionStore.getState(); const existingSession = currentState.sessions.get(sessionId); + const skillsStaleRevision = existingSession?.skillsStaleRevision; if (existingSession?.abortController) { existingSession.abortController.abort(); @@ -986,10 +987,16 @@ export function useBuildStreaming() { model, attachments ); + const currentSession = useBuildSessionStore + .getState() + .sessions.get(sessionId); updateSessionData(sessionId, { activeTurnId: turn.turn_id, activeTurnIndex: turn.turn_index, activeTurnLocalOwner: true, + ...(currentSession?.skillsStaleRevision === skillsStaleRevision && { + skillsStale: false, + }), }); await streamTurnEvents(sessionId, turn.turn_id, controller.signal); diff --git a/web/src/app/craft/v1/tasks/components/ScheduleTaskForm.tsx b/web/src/app/craft/v1/tasks/components/ScheduleTaskForm.tsx index 0e6c0d17301..abe5fb28bf6 100644 --- a/web/src/app/craft/v1/tasks/components/ScheduleTaskForm.tsx +++ b/web/src/app/craft/v1/tasks/components/ScheduleTaskForm.tsx @@ -146,27 +146,27 @@ export default function ScheduleTaskForm({ router.push(connectionPath); return; } - setSkillPicker((prev) => { - if (!prev.open) return prev; - const replacement = `${pickerEntryPromptPrefix(entry)} `; - const newPrompt = - prompt.slice(0, prev.slashIndex) + - replacement + - prompt.slice(prev.slashIndex + 1 + prev.query.length); - setPrompt(newPrompt); + if (!skillPicker.open) return; - const cursorPos = prev.slashIndex + replacement.length; - const textarea = promptTextareaRef.current; - if (textarea) { - requestAnimationFrame(() => { - textarea.focus(); - textarea.setSelectionRange(cursorPos, cursorPos); - }); - } - return { ...prev, open: false }; - }); + const replacement = `${pickerEntryPromptPrefix(entry)} `; + const newPrompt = + prompt.slice(0, skillPicker.slashIndex) + + replacement + + prompt.slice(skillPicker.slashIndex + 1 + skillPicker.query.length); + setPrompt(newPrompt); + + const cursorPos = skillPicker.slashIndex + replacement.length; + const textarea = promptTextareaRef.current; + if (textarea) { + requestAnimationFrame(() => { + textarea.focus(); + textarea.setSelectionRange(cursorPos, cursorPos); + }); + } + + setSkillPicker((prev) => (prev.open ? { ...prev, open: false } : prev)); }, - [prompt, router] + [prompt, router, skillPicker] ); const compiled = compileLocalPayloadToUtcCron(mode, payload); diff --git a/web/src/app/ee/admin/export-logs/page.tsx b/web/src/app/ee/admin/export-logs/page.tsx index d7a84caad63..32990c7e0ed 100644 --- a/web/src/app/ee/admin/export-logs/page.tsx +++ b/web/src/app/ee/admin/export-logs/page.tsx @@ -97,6 +97,11 @@ export default function ExportLogsPage() { const [isStarting, setIsStarting] = useState(false); const [isDownloading, setIsDownloading] = useState(false); const downloadedExportIdRef = useRef(null); + // Pending deferred revocation: cancelling the timer must also revoke the URL. + const pendingRevokeRef = useRef<{ + timer: ReturnType; + url: string; + } | null>(null); const { data: status, error: statusError } = useSWR( exportId === null ? null : SWR_KEYS.logExportStatus(exportId), @@ -142,6 +147,16 @@ export default function ExportLogsPage() { setExportId(null); }, [statusError]); + useEffect(() => { + return () => { + if (pendingRevokeRef.current !== null) { + clearTimeout(pendingRevokeRef.current.timer); + URL.revokeObjectURL(pendingRevokeRef.current.url); + pendingRevokeRef.current = null; + } + }; + }, []); + const downloadBundle = useCallback(async (id: string): Promise => { // Mark before any await: an attempt is in flight or succeeded, and only // failure re-arms the auto-download below. Owning this here keeps every @@ -160,7 +175,18 @@ export default function ExportLogsPage() { downloadFile(extractFilename(response), { url }); // Deferred like downloadFile's content mode: the click's download // dereferences the blob URL asynchronously. - setTimeout(() => URL.revokeObjectURL(url), 0); + if (pendingRevokeRef.current !== null) { + clearTimeout(pendingRevokeRef.current.timer); + URL.revokeObjectURL(pendingRevokeRef.current.url); + } + // Released on unmount and on replacement. The rule cannot trace the + // handle through the pendingRevokeRef object. + // oxlint-disable-next-line react-doctor/effect-needs-cleanup + const timer = setTimeout(() => { + URL.revokeObjectURL(url); + pendingRevokeRef.current = null; + }, 0); + pendingRevokeRef.current = { url, timer }; } catch (error) { console.error("Error downloading log export:", error); toast.error("Failed to download the log export."); diff --git a/web/src/app/ee/admin/standard-answer/page.tsx b/web/src/app/ee/admin/standard-answer/page.tsx index 5171587b606..4c3b490a844 100644 --- a/web/src/app/ee/admin/standard-answer/page.tsx +++ b/web/src/app/ee/admin/standard-answer/page.tsx @@ -240,8 +240,6 @@ const StandardAnswersTable = ({