diff --git a/.github/workflows/e2e-tests-playwright-template.yml b/.github/workflows/e2e-tests-playwright-template.yml
index af819e039f34..a95787282c89 100644
--- a/.github/workflows/e2e-tests-playwright-template.yml
+++ b/.github/workflows/e2e-tests-playwright-template.yml
@@ -260,13 +260,22 @@ jobs:
worker_index: ${{ fromJSON(needs.prepare-run.outputs.workers-matrix) }}
env:
COMPOSITE_IDENTITY: ${{ needs.prepare-run.outputs.composite-identity-json }}
- SERVER: "${{ inputs.server }}"
MM_LICENSE: "${{ secrets.MM_LICENSE }}"
- ENABLED_DOCKER_SERVICES: "${{ inputs.enabled_docker_services }}"
TEST: playwright
BRANCH: "${{ inputs.branch }}"
BUILD_ID: "${{ inputs.build_id }}"
CI_BASE_URL: "full-test-${{ matrix.worker_index }}"
+ # testcontainers mode: global setup brings up Postgres, Inbucket, the Mattermost
+ # server, and these services itself. Every worker enables the same fixed set because
+ # dispatch pulls specs from a shared queue, so no worker knows in advance which
+ # service-tagged spec (e.g. @ldap, @saml, @elasticsearch) it will draw.
+ PW_USE_TESTCONTAINERS: "true"
+ PW_TESTCONTAINERS_SERVICES: "openldap,keycloak,elasticsearch,opensearch,minio,azurite"
+ # Keeps the server alive across this worker's per-spec dispatch invocations instead of
+ # tearing down after each — otherwise ci/prepare-playwright's own global-teardown call would
+ # stop the server that ci/dispatch-run needs next. ci/testcontainers-teardown does the final
+ # teardown once the whole queue is done.
+ PW_TESTCONTAINERS_REUSE: "true"
steps:
- name: ci/checkout-actions
# Sparse-checkout just .github/actions from the triggering ref (master)
@@ -304,15 +313,9 @@ jobs:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('e2e-tests/playwright/package-lock.json') }}
fail-on-cache-miss: true
- - name: ci/cloud-init
- working-directory: e2e-tests
- run: make cloud-init
- - name: ci/start-server
- working-directory: e2e-tests
- run: make start-server
- # Run the `setup` project so per-spec dispatches can pass --no-deps
- # and skip plugin-load + server-deployment checks. node_modules,
- # lib/dist, and chromium are all restored from cache.
+ # Brings up the stack via Testcontainers in `testcontainers` mode (global setup). Also runs the
+ # `setup` project so per-spec dispatches can pass --no-deps and skip plugin-load +
+ # server-deployment checks. node_modules, lib/dist, and chromium are all restored from cache.
- name: ci/prepare-playwright
working-directory: e2e-tests/playwright
run: npx playwright test --project=setup
@@ -328,10 +331,6 @@ jobs:
gh-job-name: dispatch-run-${{ matrix.worker_index }}
playwright-retries: ${{ inputs.playwright_retries }}
playwright-project: ${{ inputs.playwright_project }}
- - name: ci/cloud-teardown
- if: always()
- working-directory: e2e-tests
- run: make cloud-teardown
- name: ci/upload-debug-artifacts
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
diff --git a/docs/main/administration-guide/comply/data-retention-policy.mdx b/docs/main/administration-guide/comply/data-retention-policy.mdx
index 6e4c06bc659d..6524eb668957 100644
--- a/docs/main/administration-guide/comply/data-retention-policy.mdx
+++ b/docs/main/administration-guide/comply/data-retention-policy.mdx
@@ -46,7 +46,7 @@ You can also run the deletion job manually at any time by selecting **Run Deleti
-If using data retention with [ElasticSearch](/administration-guide/scale/elasticsearch-setup), ensure the [ElasticSearch aggregate search indexes](/administration-guide/configure/environment-configuration-settings#aggregate-search-indexes) setting is set to a value that is greater than your data retention policy in days.
+If using data retention with [ElasticSearch](/deployment-guide/reference-architecture/scale/elasticsearch-setup), ensure the [ElasticSearch aggregate search indexes](/administration-guide/configure/environment-configuration-settings#aggregate-search-indexes) setting is set to a value that is greater than your data retention policy in days.
diff --git a/docs/main/administration-guide/configure/bleve-search.mdx b/docs/main/administration-guide/configure/bleve-search.mdx
index 545427ac14a7..7ca5c175075a 100644
--- a/docs/main/administration-guide/configure/bleve-search.mdx
+++ b/docs/main/administration-guide/configure/bleve-search.mdx
@@ -7,7 +7,7 @@ title: "Bleve search"
**From Mattermost v11, Bleve search has been deprecated.**
-- **For v11.0 and later**: Use [Elasticsearch](/administration-guide/scale/elasticsearch-setup) or [OpenSearch](/administration-guide/scale/opensearch-setup) for [enterprise search](/administration-guide/scale/enterprise-search) capabilities
+- **For v11.0 and later**: Use [Elasticsearch](/deployment-guide/reference-architecture/scale/elasticsearch-setup) or [OpenSearch](/deployment-guide/reference-architecture/scale/opensearch-setup) for [enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search) capabilities
- **For pre-v11.0 deployments**: This documentation remains relevant for existing installations that continue using Bleve.
@@ -61,4 +61,4 @@ The following conditions are applied when using Bleve search:
## How does search work with Bleve disabled?
-Mattermost performs full text searches against the database unless you have an [Enterprise license](/product-overview/editions-and-offerings#mattermost-enterprise) and [enterprise search](/administration-guide/scale/enterprise-search) configured.
+Mattermost performs full text searches against the database unless you have an [Enterprise license](/product-overview/editions-and-offerings#mattermost-enterprise) and [enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search) configured.
diff --git a/docs/main/administration-guide/configure/configuration-in-your-database.mdx b/docs/main/administration-guide/configure/configuration-in-your-database.mdx
index 2056c43df12a..1959c034d40a 100644
--- a/docs/main/administration-guide/configure/configuration-in-your-database.mdx
+++ b/docs/main/administration-guide/configure/configuration-in-your-database.mdx
@@ -3,7 +3,7 @@ title: "Store configuration in your database"
---
-If you have a self-hosted Mattermost deployment, you can use your database as the single source of truth for the active configuration of your Mattermost installation. This changes the Mattermost binary from reading the default `config.json` file to reading the configuration settings stored within a configuration table in the database. Mattermost has been running our [community server](https://community.mattermost.com) on this option since the feature was released, and recommends its use for those on [High Availability deployments](/administration-guide/scale/high-availability-cluster-based-deployment).
+If you have a self-hosted Mattermost deployment, you can use your database as the single source of truth for the active configuration of your Mattermost installation. This changes the Mattermost binary from reading the default `config.json` file to reading the configuration settings stored within a configuration table in the database. Mattermost has been running our [community server](https://community.mattermost.com) on this option since the feature was released, and recommends its use for those on [High Availability deployments](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment).
Benefits to using this option:
diff --git a/docs/main/administration-guide/configure/environment-configuration-settings.mdx b/docs/main/administration-guide/configure/environment-configuration-settings.mdx
index b1a01e868c2e..b77c37f4f1d0 100644
--- a/docs/main/administration-guide/configure/environment-configuration-settings.mdx
+++ b/docs/main/administration-guide/configure/environment-configuration-settings.mdx
@@ -333,7 +333,7 @@ When using the Mattermost Desktop App, additional configuration is required to o
-Purging the caches may adversely impact performance. [high availability cluster-based deployments](/administration-guide/scale/high-availability-cluster-based-deployment) will attempt to purge all the servers in the cluster.
+Purging the caches may adversely impact performance. [high availability cluster-based deployments](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) will attempt to purge all the servers in the cluster.
@@ -697,7 +697,7 @@ Your database admin must configure the functionality according to supported valu
#### AWS High Availablity RDS cluster deployments
-For an AWS High Availability RDS cluster deployment, point this configuration setting to the write/read endpoint at the **cluster** level to benefit from the AWS failover handling. AWS takes care of promoting different database nodes to be the writer node. Mattermost doesn't need to manage this. See the [high availability database configuration](/administration-guide/scale/high-availability-cluster-based-deployment#database) documentation for details.
+For an AWS High Availability RDS cluster deployment, point this configuration setting to the write/read endpoint at the **cluster** level to benefit from the AWS failover handling. AWS takes care of promoting different database nodes to be the writer node. Mattermost doesn't need to manage this. See the [high availability database configuration](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment#database) documentation for details.
### Maximum open connections
@@ -842,7 +842,7 @@ MySQL databases must be configured to support searching strings shorter than thr
Search behavior in Mattermost depends on which search engines are enabled:
-- When [Elasticsearch](/administration-guide/scale/elasticsearch-setup) or [AWS OpenSearch](/administration-guide/scale/opensearch-setup) is enabled, Mattermost will try to use it first.
+- When [Elasticsearch](/deployment-guide/reference-architecture/scale/elasticsearch-setup) or [AWS OpenSearch](/deployment-guide/reference-architecture/scale/opensearch-setup) is enabled, Mattermost will try to use it first.
- If Elasticsearch fails or is disabled, Mattermost will attempt to use Bleve search, if enabled. Bleve search has been deprecated in Mattermost v11.0. We recommend using Elasticsearch or OpenSearch for enterprise search capabilities.
- If these fail or are disabled, Mattermost tries to search the database directly, if this is enabled.
- If all of the above methods fail or are disabled, the search results will be empty.
@@ -893,7 +893,7 @@ Read-only display of the currently active backend used for search. Values can in
#### AWS High Availability RDS cluster deployments
-For an AWS High Availability RDS cluster deployment, point this configuration setting directly to the underlying read-only node endpoint within the RDS cluster to circumvent the failover/load balancing that AWS/RDS takes care of (except for the write traffic). Mattermost has its own method of balancing the read-only connections and can also balance those queries to the data source/write+read connection should those nodes fail. See the [high availability database configuration](/administration-guide/scale/high-availability-cluster-based-deployment#database) documentation for details.
+For an AWS High Availability RDS cluster deployment, point this configuration setting directly to the underlying read-only node endpoint within the RDS cluster to circumvent the failover/load balancing that AWS/RDS takes care of (except for the write traffic). Mattermost has its own method of balancing the read-only connections and can also balance those queries to the data source/write+read connection should those nodes fail. See the [high availability database configuration](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment#database) documentation for details.
### Search replicas
@@ -918,7 +918,7 @@ Each database connection string in the array must be in the same form used for t
#### AWS High Availability RDS cluster deployments
-For an AWS High Availability RDS cluster deployment, point this configuration setting directly to the underlying read-only node endpoint within the RDS cluster to circumvent the failover/load balancing that AWS/RDS takes care of (except for the write traffic). Mattermost has its own method of balancing the read-only connections and can also balance those queries to the data source/write+read connection should those nodes fail. See the [high availability database configuration](/administration-guide/scale/high-availability-cluster-based-deployment#database) documentation for details.
+For an AWS High Availability RDS cluster deployment, point this configuration setting directly to the underlying read-only node endpoint within the RDS cluster to circumvent the failover/load balancing that AWS/RDS takes care of (except for the write traffic). Mattermost has its own method of balancing the read-only connections and can also balance those queries to the data source/write+read connection should those nodes fail. See the [high availability database configuration](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment#database) documentation for details.
### Replica lag settings
@@ -1096,7 +1096,7 @@ If indexing is disabled and then re-enabled after an index is created, purge and
### Backend type
-Both [Elasticsearch](/administration-guide/scale/elasticsearch-setup) and [AWS OpenSearch](/administration-guide/scale/opensearch-setup) provide enterprise-scale deployments with optimized search performance and prevents performance degradation and timeouts. Learn more about [enterprise search](/administration-guide/scale/enterprise-search) in our product documentation.
+Both [Elasticsearch](/deployment-guide/reference-architecture/scale/elasticsearch-setup) and [AWS OpenSearch](/deployment-guide/reference-architecture/scale/opensearch-setup) provide enterprise-scale deployments with optimized search performance and prevents performance degradation and timeouts. Learn more about [enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search) in our product documentation.
@@ -1111,7 +1111,7 @@ Both [Elasticsearch](/administration-guide/scale/elasticsearch-setup) and [AWS O
-Learn more about [enterprise search version support](/administration-guide/scale/enterprise-search#supported-paths).
+Learn more about [enterprise search version support](/deployment-guide/reference-architecture/scale/enterprise-search#supported-paths).
### Server connection address
@@ -1507,7 +1507,7 @@ If there are `n` data nodes, the number of replicas per shard for each index sho
-If you’re using [data retention](/administration-guide/comply/data-retention-policy) and [enterprise search](/administration-guide/scale/enterprise-search), configure this with a value greater than your data retention policy.
+If you’re using [data retention](/administration-guide/comply/data-retention-policy) and [enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search), configure this with a value greater than your data retention policy.
@@ -1698,9 +1698,9 @@ During busy periods, this delay will be faster as more traffic is occurring, cau
-Available from Mattermost v11.6. The required analyzer plugins should be installed on the Elasticsearch or AWS OpenSearch server before enabling this setting for full analysis support. If no plugin is detected, a warning will be logged. See the [Elasticsearch setup](/administration-guide/scale/elasticsearch-setup) and [AWS OpenSearch setup](/administration-guide/scale/opensearch-setup) documentation for plugin installation instructions.
+Available from Mattermost v11.6. The required analyzer plugins should be installed on the Elasticsearch or AWS OpenSearch server before enabling this setting for full analysis support. If no plugin is detected, a warning will be logged. See the [Elasticsearch setup](/deployment-guide/reference-architecture/scale/elasticsearch-setup) and [AWS OpenSearch setup](/deployment-guide/reference-architecture/scale/opensearch-setup) documentation for plugin installation instructions.
-If you enable this setting on a server that was previously running Elasticsearch or AWS OpenSearch, you must purge and rebuild the search indexes for existing content to be properly searchable with the new analyzers. See the [Elasticsearch setup](/administration-guide/scale/elasticsearch-setup) documentation for instructions on purging and rebuilding indexes.
+If you enable this setting on a server that was previously running Elasticsearch or AWS OpenSearch, you must purge and rebuild the search indexes for existing content to be properly searchable with the new analyzers. See the [Elasticsearch setup](/deployment-guide/reference-architecture/scale/elasticsearch-setup) documentation for instructions on purging and rebuilding indexes.
@@ -2514,7 +2514,7 @@ See the [Telemetry](/administration-guide/manage/telemetry#security-update-check
-With self-hosted deployments, you can configure Mattermost as a [high availability cluster-based deployment](/administration-guide/scale/high-availability-cluster-based-deployment) by going to **System Console \> Environment \> High Availability**, or by editing the `config.json` file as described in the following tables. Changes to configuration settings in this section require a server restart before taking effect.
+With self-hosted deployments, you can configure Mattermost as a [high availability cluster-based deployment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) by going to **System Console \> Environment \> High Availability**, or by editing the `config.json` file as described in the following tables. Changes to configuration settings in this section require a server restart before taking effect.
In a Mattermost high availability cluster-based deployment, the System Console is set to read-only, and settings can only be changed by editing the `config.json` file directly. However, to test a high availability cluster-based environment, you can disable `ClusterSettings.ReadOnlyConfig` in the `config.json` file by setting it to `false`. This allows changes applied using the System Console to be saved back to the configuration file.
@@ -2563,7 +2563,7 @@ In a Mattermost high availability cluster-based deployment, the System Console i
-See the [high availability cluster-based deployment](/administration-guide/scale/high-availability-cluster-based-deployment) documentation for details.
+See the [high availability cluster-based deployment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) documentation for details.
### Use IP address
@@ -3337,7 +3337,7 @@ After changing this setting, the new session length takes effect after the next
- This setting has no effect when [extend session length with activity](#extend-session-length-with-activity) is set to **true**.
- This setting applies to the webapp and the desktop app. For mobile apps, use an [EMM provider](/deployment-guide/mobile/deploy-mobile-apps-using-emm-provider) to lock the app when not in use. |
-- In [high availability mode](/administration-guide/scale/high-availability-cluster-based-deployment), enable IP hash load balancing for reliable timeout measurement.
+- In [high availability mode](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment), enable IP hash load balancing for reliable timeout measurement.
diff --git a/docs/main/administration-guide/configure/experimental-configuration-settings.mdx b/docs/main/administration-guide/configure/experimental-configuration-settings.mdx
index 69fe4d858aa0..cea1c2811e0d 100644
--- a/docs/main/administration-guide/configure/experimental-configuration-settings.mdx
+++ b/docs/main/administration-guide/configure/experimental-configuration-settings.mdx
@@ -645,7 +645,7 @@ This setting resolves issues where YouTube video previews display as unavailable
-**From Mattermost v11, Bleve search has been deprecated.** These configuration settings are only available for Mattermost versions prior to v11.0. For v11.0 and later, [Elasticsearch](/administration-guide/scale/elasticsearch-setup) or [OpenSearch](/administration-guide/scale/opensearch-setup) for [enterprise search](/administration-guide/scale/enterprise-search) capabilities.
+**From Mattermost v11, Bleve search has been deprecated.** These configuration settings are only available for Mattermost versions prior to v11.0. For v11.0 and later, [Elasticsearch](/deployment-guide/reference-architecture/scale/elasticsearch-setup) or [OpenSearch](/deployment-guide/reference-architecture/scale/opensearch-setup) for [enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search) capabilities.
@@ -1554,7 +1554,7 @@ This setting isn't available in the System Console and can only be set in `confi
Set whether or not this Mattermost server will handle tasks created by the Scheduler. When running Mattermost on a single machine, this setting should always be enabled.
-When running Mattermost in [High Availablity mode](/administration-guide/scale/high-availability-cluster-based-deployment), one or more servers should have this setting enabled. We recommend that your High Availability cluster-based deployment has one or more dedicated Workers with this setting enabled while the remaining Mattermost app servers have it disabled.
+When running Mattermost in [High Availablity mode](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment), one or more servers should have this setting enabled. We recommend that your High Availability cluster-based deployment has one or more dedicated Workers with this setting enabled while the remaining Mattermost app servers have it disabled.
@@ -1573,7 +1573,7 @@ This setting isn't available in the System Console and can only be set in `confi
Set whether or not this Mattermost server will schedule tasks that will be completed by a Worker. When running Mattermost on a single machine, this setting should always be enabled.
-When running Mattermost in [High Availablity mode](/administration-guide/scale/high-availability-cluster-based-deployment), this setting should always be enabled. In a High Availability cluster-based deployment, exactly one of the servers will be designated as the Scheduler at a time to ensure that duplicate tasks aren't created. See [High Availability documentation](/administration-guide/scale/high-availability-cluster-based-deployment) for more details.
+When running Mattermost in [High Availablity mode](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment), this setting should always be enabled. In a High Availability cluster-based deployment, exactly one of the servers will be designated as the Scheduler at a time to ensure that duplicate tasks aren't created. See [High Availability documentation](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) for more details.
diff --git a/docs/main/administration-guide/configure/plugins-configuration-settings.mdx b/docs/main/administration-guide/configure/plugins-configuration-settings.mdx
index 2a3d4a1cf1ed..5d50cb7c20f0 100644
--- a/docs/main/administration-guide/configure/plugins-configuration-settings.mdx
+++ b/docs/main/administration-guide/configure/plugins-configuration-settings.mdx
@@ -437,7 +437,7 @@ Use this setting as a system admin to confirm calls work as expected. When **fal
-\- This setting is applicable only to self-hosted deployments. - The environment variable `MM_CALLS_MAX_PARTICIPANTS` is deprecated in favor of `MM_CALLS_MAX_CALL_PARTICIPANTS`. - This setting is optional, but the recommended maximum number of participants is **50**. Call participant limits greatly depends on instance resources. See the [Calls deployment guide](/administration-guide/configure/calls-deployment-guide) documentation for details.
+\- This setting is applicable only to self-hosted deployments. - The environment variable `MM_CALLS_MAX_PARTICIPANTS` is deprecated in favor of `MM_CALLS_MAX_CALL_PARTICIPANTS`. - This setting is optional, but the recommended maximum number of participants is **50**. Call participant limits greatly depends on instance resources. See the [Calls deployment guide](/deployment-guide/calls/calls-deployment-guide) documentation for details.
@@ -458,7 +458,7 @@ Use this setting as a system admin to confirm calls work as expected. When **fal
-\- This setting is applicable only to self-hosted deployments. - The configurations above, containing STUN and TURN servers, are sent to the clients and used to generate local candidates. - If hosting calls through the plugin (i.e. not using the [RTCD service](/administration-guide/configure/calls-deployment-guide)) any configured STUN server may also be used to find the instance's public IP when none is provided through the [ICE Host Override](/administration-guide/configure/plugins-configuration-settings#ice-host-override) option.
+\- This setting is applicable only to self-hosted deployments. - The configurations above, containing STUN and TURN servers, are sent to the clients and used to generate local candidates. - If hosting calls through the plugin (i.e. not using the [RTCD service](/deployment-guide/calls/calls-deployment-guide)) any configured STUN server may also be used to find the instance's public IP when none is provided through the [ICE Host Override](/administration-guide/configure/plugins-configuration-settings#ice-host-override) option.
@@ -693,7 +693,7 @@ This setting is applicable only to self-hosted deployments.
-\- This setting is applicable only to self-hosted deployments. - The quality setting will affect the performance of the job service and the file size of recordings. Refer to the [Calls deployment guide](/administration-guide/configure/calls-deployment-guide) documentation for more information.
+\- This setting is applicable only to self-hosted deployments. - The quality setting will affect the performance of the job service and the file size of recordings. Refer to the [Calls deployment guide](/deployment-guide/calls/calls-deployment-guide) documentation for more information.
@@ -739,7 +739,7 @@ This setting is applicable only to self-hosted deployments.
-\- This setting is applicable only to self-hosted deployments. - This setting is available starting in plugin version 0.22. The model size setting will affect the performance of the job service. Refer to the [Calls deployment guide](/administration-guide/configure/calls-deployment-guide) documentation for more information.
+\- This setting is applicable only to self-hosted deployments. - This setting is available starting in plugin version 0.22. The model size setting will affect the performance of the job service. Refer to the [Calls deployment guide](/deployment-guide/calls/calls-deployment-guide) documentation for more information.
@@ -762,7 +762,7 @@ This setting is applicable only to self-hosted deployments.
-\- This setting is applicable only to self-hosted deployments. - The call transcriber threads setting will affect the performance of the job service. Refer to the [Calls deployment guide](/administration-guide/configure/calls-deployment-guide) documentation for more information. This setting is available starting in plugin version 0.26.2.
+\- This setting is applicable only to self-hosted deployments. - The call transcriber threads setting will affect the performance of the job service. Refer to the [Calls deployment guide](/deployment-guide/calls/calls-deployment-guide) documentation for more information. This setting is available starting in plugin version 0.26.2.
@@ -966,7 +966,7 @@ The ability to enable video calls in DMs is currently [Experimental](/administra
-If your deployment offloads calls to the [RTCD service](/administration-guide/configure/calls-rtcd-setup), video in DM calls requires **rtcd v1.2.2 or later**. We recommend running the rtcd version that ships with your Calls plugin release (rtcd v1.2.5 for Calls v1.12.0) and keeping rtcd updated alongside the plugin. Deployments that use the plugin's built-in RTC server (no RTCD) support video without additional configuration.
+If your deployment offloads calls to the [RTCD service](/deployment-guide/calls/calls-rtcd-setup), video in DM calls requires **rtcd v1.2.2 or later**. We recommend running the rtcd version that ships with your Calls plugin release (rtcd v1.2.5 for Calls v1.12.0) and keeping rtcd updated alongside the plugin. Deployments that use the plugin's built-in RTC server (no RTCD) support video without additional configuration.
@@ -987,7 +987,7 @@ If your deployment offloads calls to the [RTCD service](/administration-guide/co
-- Version v0.18.0 or higher of the [RTCD service](/administration-guide/configure/calls-deployment-guide) is required for this functionality to work when hosting calls through the dedicated WebRTC service.
+- Version v0.18.0 or higher of the [RTCD service](/deployment-guide/calls/calls-deployment-guide) is required for this functionality to work when hosting calls through the dedicated WebRTC service.
- Use caution when enabling this experimental configuration setting since it determines how the system handles part of the setup for WebRTC-based calls. Enabling this configuration setting may make the call setup a bit faster or more reliable in certain situations.
diff --git a/docs/main/administration-guide/manage/admin/generating-support-packet.mdx b/docs/main/administration-guide/manage/admin/generating-support-packet.mdx
index f3e46d638665..f931248f8a59 100644
--- a/docs/main/administration-guide/manage/admin/generating-support-packet.mdx
+++ b/docs/main/administration-guide/manage/admin/generating-support-packet.mdx
@@ -162,11 +162,11 @@ The following additional plugin diagnostic data is included in the generated Sup
-From Mattermost v10.10, Support Packets from [high availability](/administration-guide/scale/high-availability-cluster-based-deployment) deployments organize cluster-specific files (such as log files) in subdirectories named after each cluster node, while cluster-wide files remain in the root directory.
+From Mattermost v10.10, Support Packets from [high availability](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) deployments organize cluster-specific files (such as log files) in subdirectories named after each cluster node, while cluster-wide files remain in the root directory.
Support packet file organization has been improved to make it easier to identify cluster-wide versus cluster-specific files:
-- **Cluster-wide files** (identical across all nodes in a [high-availability cluster](/administration-guide/scale/high-availability-cluster-based-deployment)) remain in the root directory of the support packet.
+- **Cluster-wide files** (identical across all nodes in a [high-availability cluster](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment)) remain in the root directory of the support packet.
- **Cluster-specific files** (unique per node) are now organized in subdirectories named after each cluster node.
**Cluster-wide files (root directory):**
diff --git a/docs/main/administration-guide/manage/admin/installing-license-key.mdx b/docs/main/administration-guide/manage/admin/installing-license-key.mdx
index 23a99d007067..b2d84571c2d2 100644
--- a/docs/main/administration-guide/manage/admin/installing-license-key.mdx
+++ b/docs/main/administration-guide/manage/admin/installing-license-key.mdx
@@ -19,7 +19,7 @@ Once the key is uploaded and installed, the details of your license are displaye
-Use the [mmctl license upload](/administration-guide/manage/mmctl-command-line-tool#mmctl-license-upload) command to upload a new license or replace an existing license file with a new one. When complete, restart the Mattermost server. If you're running in a [High Availability](/administration-guide/scale/high-availability-cluster-based-deployment) environment, the new license file must be updated to every node.
+Use the [mmctl license upload](/administration-guide/manage/mmctl-command-line-tool#mmctl-license-upload) command to upload a new license or replace an existing license file with a new one. When complete, restart the Mattermost server. If you're running in a [High Availability](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) environment, the new license file must be updated to every node.
``` sh
mmctl license upload [license] [flags]
@@ -34,7 +34,7 @@ mmctl license upload [license] [flags]
- From Mattermost v10.11, the option to add a license is disabled when the license is set using an [environment variable](/administration-guide/configure/environment-configuration-settings#license-file-location).
- Enterprise customers with the Premier Support add-on can request a staging license for testing.
- Removing a Mattermost Enterprise or Professional license key won't remove the configuration for Enterprise settings; however, these features won't function until an Enterprise or Professional license key is applied.
-- When you're using [High Availability](/administration-guide/scale/high-availability-cluster-based-deployment), it's critical to ensure that all servers in the cluster have same Enterprise license properly installed to prevent multi-node clusters from failing. An Enterprise license is required for High Availability to work.
+- When you're using [High Availability](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment), it's critical to ensure that all servers in the cluster have same Enterprise license properly installed to prevent multi-node clusters from failing. An Enterprise license is required for High Availability to work.
- When you apply an Enterprise license key to a server previously licensed for Professional, Professional features retain their configuration settings in Enterprise.
- When you apply a Professional license to a server previously licensed for Enterprise, Enterprise features retain their configuration but will no longer be accessible for use.
diff --git a/docs/main/administration-guide/manage/command-line-tools.mdx b/docs/main/administration-guide/manage/command-line-tools.mdx
index 75cdc5d78db6..9511d00bd05d 100644
--- a/docs/main/administration-guide/manage/command-line-tools.mdx
+++ b/docs/main/administration-guide/manage/command-line-tools.mdx
@@ -101,7 +101,7 @@ The Docker Install tab details and command references below also apply to the [M
-\- The CLI is run in a single node which bypasses the mechanisms that a [High Availability environment](/administration-guide/scale/high-availability-cluster-based-deployment) uses to perform actions across all nodes in the cluster. As a result, when running [CLI commands](/administration-guide/manage/command-line-tools) in a High Availability environment, tasks that change configuration settings require a server restart. - Parameters in CLI commands are order-specific. - If special characters (`!`, `|`, `(`, `)`, `\`, `'`, or `"`) are used, the entire argument needs to be surrounded by single quotes, or the individual characters need to be escaped out.
+\- The CLI is run in a single node which bypasses the mechanisms that a [High Availability environment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) uses to perform actions across all nodes in the cluster. As a result, when running [CLI commands](/administration-guide/manage/command-line-tools) in a High Availability environment, tasks that change configuration settings require a server restart. - Parameters in CLI commands are order-specific. - If special characters (`!`, `|`, `(`, `)`, `\`, `'`, or `"`) are used, the entire argument needs to be surrounded by single quotes, or the individual characters need to be escaped out.
diff --git a/docs/main/administration-guide/manage/configure-health-check-probes.mdx b/docs/main/administration-guide/manage/configure-health-check-probes.mdx
index cb0c762a1d36..acdc9e52425f 100644
--- a/docs/main/administration-guide/manage/configure-health-check-probes.mdx
+++ b/docs/main/administration-guide/manage/configure-health-check-probes.mdx
@@ -9,7 +9,7 @@ Before you begin, you should have a running Mattermost server. If you don't, you
-[Highly available Mattermost cluster support](/administration-guide/scale/high-availability-cluster-based-deployment) requires Mattermost Enterprise.
+[Highly available Mattermost cluster support](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) requires Mattermost Enterprise.
diff --git a/docs/main/administration-guide/manage/logging.mdx b/docs/main/administration-guide/manage/logging.mdx
index ad2316225a0e..f1c3056044fc 100644
--- a/docs/main/administration-guide/manage/logging.mdx
+++ b/docs/main/administration-guide/manage/logging.mdx
@@ -1046,7 +1046,7 @@ The TCP socket targets can be configured with an IP address or domain name, port
-From Mattermost v11.4, debug-level log messages are available to help system admins understand cluster job execution behavior in [high availability](/administration-guide/scale/high-availability-cluster-based-deployment) deployments for specific Recurring Tasks.
+From Mattermost v11.4, debug-level log messages are available to help system admins understand cluster job execution behavior in [high availability](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) deployments for specific Recurring Tasks.
These debug messages apply only to the following Recurring Tasks:
@@ -1097,7 +1097,7 @@ These debug messages only apply to Recurring Tasks. For other job types (Elastic
-For more information about leader election and cluster configuration, see [High Availability cluster-based deployment](/administration-guide/scale/high-availability-cluster-based-deployment).
+For more information about leader election and cluster configuration, see [High Availability cluster-based deployment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment).
------------------------------------------------------------------------------------------------------------------------
diff --git a/docs/main/administration-guide/manage/statistics.mdx b/docs/main/administration-guide/manage/statistics.mdx
index 756bd63b2866..5c86963394df 100644
--- a/docs/main/administration-guide/manage/statistics.mdx
+++ b/docs/main/administration-guide/manage/statistics.mdx
@@ -72,7 +72,7 @@ Master DB Conns
The number of active connections currently on your master database.
Replica DB Conns
-The number of active connections currently on one or more of [your read replica databases](/administration-guide/scale/high-availability-cluster-based-deployment#database).
+The number of active connections currently on one or more of [your read replica databases](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment#database).
Total Playbooks
The total number of collaborative playbooks on this server.
diff --git a/docs/main/administration-guide/scale/deploy-prometheus-grafana-for-performance-monitoring.mdx b/docs/main/administration-guide/scale/deploy-prometheus-grafana-for-performance-monitoring.mdx
index b2c834e52f8c..f4669dfaddb0 100644
--- a/docs/main/administration-guide/scale/deploy-prometheus-grafana-for-performance-monitoring.mdx
+++ b/docs/main/administration-guide/scale/deploy-prometheus-grafana-for-performance-monitoring.mdx
@@ -3,7 +3,7 @@ title: "Deploy Prometheus and Grafana for performance monitoring"
---
-Performance monitoring support enables admins to track system health for large Enterprise deployments through integrations with [Prometheus](https://prometheus.io/) and [Grafana](https://grafana.com/). These integrations support data collection from several Mattermost servers, which is particularly useful if you're running Mattermost [in high availability mode](/administration-guide/scale/high-availability-cluster-based-deployment). Once you're tracking system health, you can [set up performance alerts](/administration-guide/scale/performance-alerting) on your Grafana dashboard.
+Performance monitoring support enables admins to track system health for large Enterprise deployments through integrations with [Prometheus](https://prometheus.io/) and [Grafana](https://grafana.com/). These integrations support data collection from several Mattermost servers, which is particularly useful if you're running Mattermost [in high availability mode](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment). Once you're tracking system health, you can [set up performance alerts](/administration-guide/scale/performance-alerting) on your Grafana dashboard.
Admins can collect and store various data points from the Mattermost application in an [OpenMetrics](https://openmetrics.io) format by [deploying Prometheus](#install-prometheus) and [Grafana](#install-grafana).
diff --git a/docs/main/administration-guide/upgrade/admin-onboarding-tasks.mdx b/docs/main/administration-guide/upgrade/admin-onboarding-tasks.mdx
index c771c3d49307..8ce3ada47fe5 100644
--- a/docs/main/administration-guide/upgrade/admin-onboarding-tasks.mdx
+++ b/docs/main/administration-guide/upgrade/admin-onboarding-tasks.mdx
@@ -135,10 +135,10 @@ Email notifications can be batched together so users don’t get overwhelmed wit
Enable email notifications first by setting **System Console \> Notifications \> Enable Email Notifications** to **true**. See the [Enable email notifications](/administration-guide/configure/site-configuration-settings#enable-email-notifications) configuration settings documentation for details. Note that email notifications require an [SMTP email server](/administration-guide/configure/environment-configuration-settings#smtp-server) to be configured.
-Then, enable batched email notifications by setting **System Console \> Notifications \> Enable Email Batching** to **true**. See the [Enable email batching](/administration-guide/configure/site-configuration-settings#enable-email-batching) configuration settings documentation for details. Note that email batching is not available if you are running your deployment in [High Availability](/administration-guide/scale/high-availability-cluster-based-deployment).
+Then, enable batched email notifications by setting **System Console \> Notifications \> Enable Email Batching** to **true**. See the [Enable email batching](/administration-guide/configure/site-configuration-settings#enable-email-batching) configuration settings documentation for details. Note that email batching is not available if you are running your deployment in [High Availability](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment).
**9. Enable Elasticsearch**
-Mattermost Enterprise customers can enable [enterprise search](/administration-guide/scale/enterprise-search) for optimized search performance at enterprise-scale. Both Elasticsearch and AWS OpenSearch solve many known issues with full text database search, such as dots, dashes, and email addresses returning unexpected results.
+Mattermost Enterprise customers can enable [enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search) for optimized search performance at enterprise-scale. Both Elasticsearch and AWS OpenSearch solve many known issues with full text database search, such as dots, dashes, and email addresses returning unexpected results.
-Enable Elasticsearch by setting **System Console \> Elasticsearch \> Enable Indexing** to **true**. See the [Elasticsearch](/administration-guide/configure/environment-configuration-settings#enterprise-search) configuration settings documentation for details. Enabling Elasticsearch requires [setting up an Elasticsearch server](/administration-guide/scale/elasticsearch-setup#set-up-elasticsearch).
+Enable Elasticsearch by setting **System Console \> Elasticsearch \> Enable Indexing** to **true**. See the [Elasticsearch](/administration-guide/configure/environment-configuration-settings#enterprise-search) configuration settings documentation for details. Enabling Elasticsearch requires [setting up an Elasticsearch server](/deployment-guide/reference-architecture/scale/elasticsearch-setup#set-up-elasticsearch).
diff --git a/docs/main/administration-guide/upgrade/prepare-to-upgrade-mattermost.mdx b/docs/main/administration-guide/upgrade/prepare-to-upgrade-mattermost.mdx
index 8e55c17c4463..c6370b37b4ef 100644
--- a/docs/main/administration-guide/upgrade/prepare-to-upgrade-mattermost.mdx
+++ b/docs/main/administration-guide/upgrade/prepare-to-upgrade-mattermost.mdx
@@ -111,9 +111,9 @@ UPDATE TableName SET ColumnName = regexp_replace(ColumnName, '\\u0000', '', 'g')
## Upgrade high availability cluster-based deployments
-In [high availability cluster-based](/administration-guide/scale/high-availability-cluster-based-deployment) environments, you should expect to schedule downtime for the upgrade to v6.0. Based on your database size and setup, the migration to v6.0 can take a significant amount of time, and may even lock the tables for posts which will prevent your users from posting or receiving messages until the migration is complete.
+In [high availability cluster-based](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) environments, you should expect to schedule downtime for the upgrade to v6.0. Based on your database size and setup, the migration to v6.0 can take a significant amount of time, and may even lock the tables for posts which will prevent your users from posting or receiving messages until the migration is complete.
-Ensure you review the [high availability cluster-based deployment upgrade guide](/administration-guide/scale/high-availability-cluster-based-deployment#upgrade-guide), as well as the [important upgrade notes](/administration-guide/upgrade/important-upgrade-notes) to make sure you're aware of any actions you need to take before or after upgrading from your particular version.
+Ensure you review the [high availability cluster-based deployment upgrade guide](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment#upgrade-guide), as well as the [important upgrade notes](/administration-guide/upgrade/important-upgrade-notes) to make sure you're aware of any actions you need to take before or after upgrading from your particular version.
diff --git a/docs/main/administration-guide/upgrade/upgrade-mattermost-kubernetes-ha.mdx b/docs/main/administration-guide/upgrade/upgrade-mattermost-kubernetes-ha.mdx
index 9485a055c541..29b79648f210 100644
--- a/docs/main/administration-guide/upgrade/upgrade-mattermost-kubernetes-ha.mdx
+++ b/docs/main/administration-guide/upgrade/upgrade-mattermost-kubernetes-ha.mdx
@@ -17,7 +17,7 @@ Health monitoring ensures that only healthy pods are replaced, and new pods are
### High Availability
-In [High Availability (HA) cluster-based deployments](/administration-guide/scale/high-availability-cluster-based-deployment), Mattermost runs multiple application servers in a cluster. This configuration ensures that if one server fails, others can continue to serve requests without downtime. User traffic load balancing is managed with services such as NGINX Ingress or HAProxy. [PostgreSQL](/deployment-guide/server/preparations#database-preparation) and [file storage](/deployment-guide/server/preparations#file-storage-preparation) are deployed with replication for redundancy and failover.
+In [High Availability (HA) cluster-based deployments](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment), Mattermost runs multiple application servers in a cluster. This configuration ensures that if one server fails, others can continue to serve requests without downtime. User traffic load balancing is managed with services such as NGINX Ingress or HAProxy. [PostgreSQL](/deployment-guide/server/preparations#database-preparation) and [file storage](/deployment-guide/server/preparations#file-storage-preparation) are deployed with replication for redundancy and failover.
### Active/Active deployments
diff --git a/docs/main/administration-guide/upgrade/upgrading-mattermost-server.mdx b/docs/main/administration-guide/upgrade/upgrading-mattermost-server.mdx
index c6f32f9c532a..a7062e59fb24 100644
--- a/docs/main/administration-guide/upgrade/upgrading-mattermost-server.mdx
+++ b/docs/main/administration-guide/upgrade/upgrading-mattermost-server.mdx
@@ -52,7 +52,7 @@ To learn how to safely upgrade your deployment in Kubernetes for High Availabili
-**Before upgrading to Mattermost v11.0**: If you're currently using Bleve search (found under **System Console \> Experimental \> Bleve**), ensure that `DisableDatabaseSearch` is set to `false` before upgrading, or search will become non-functional after the upgrade. Bleve search has been removed in v11.0. For enterprise search capabilities, consider migrating to [Elasticsearch](/administration-guide/scale/elasticsearch-setup) or [OpenSearch](/administration-guide/scale/opensearch-setup) for [enterprise search](/administration-guide/scale/enterprise-search) capabilities.
+**Before upgrading to Mattermost v11.0**: If you're currently using Bleve search (found under **System Console \> Experimental \> Bleve**), ensure that `DisableDatabaseSearch` is set to `false` before upgrading, or search will become non-functional after the upgrade. Bleve search has been removed in v11.0. For enterprise search capabilities, consider migrating to [Elasticsearch](/deployment-guide/reference-architecture/scale/elasticsearch-setup) or [OpenSearch](/deployment-guide/reference-architecture/scale/opensearch-setup) for [enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search) capabilities.
@@ -252,7 +252,7 @@ wget https://releases.mattermost.com/X.X.X/mattermost-team-X.X.X-linux-amd64.tar
sudo rm -i /tmp/mattermost*.gz
```
-13. If you're using a [high availability](/administration-guide/scale/high-availability-cluster-based-deployment) deployment, you need to apply the steps above on every node in your cluster. Once complete, the **Config File MD5** columns in the high availability section of the System Console should be green. If they're yellow, please ensure that all nodes have the same server version and the same configuration.
+13. If you're using a [high availability](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) deployment, you need to apply the steps above on every node in your cluster. Once complete, the **Config File MD5** columns in the high availability section of the System Console should be green. If they're yellow, please ensure that all nodes have the same server version and the same configuration.
If they continue to display as yellow, trigger a configuration propagation across the cluster by opening the System Console, changing a setting, and reverting it. This will enable the **Save** button for that page. Then, select **Save**. This will not change any configuration, but sends the existing configuration to all nodes in the cluster.
diff --git a/docs/main/deployment-guide/air-gapped-operations/mirror-package-repositories.mdx b/docs/main/deployment-guide/air-gapped-operations/mirror-package-repositories.mdx
index 72ec74556a5d..d6f458ad4d2c 100644
--- a/docs/main/deployment-guide/air-gapped-operations/mirror-package-repositories.mdx
+++ b/docs/main/deployment-guide/air-gapped-operations/mirror-package-repositories.mdx
@@ -32,7 +32,7 @@ Using our provided tarball is recommended as the simplest installation method fo
Consider downloading these additional resources if you plan to enable these optional components:
-- [Mattermost Calls](/administration-guide/configure/calls-deployment-guide): [mattermost-calls-offloader](https://github.com/mattermost/calls-offloader/releases) (required for recording, transcription, and live captions) and [mattermost-rtcd](https://github.com/mattermost/rtcd/releases) (required for performance and scalability).
+- [Mattermost Calls](/deployment-guide/calls/calls-deployment-guide): [mattermost-calls-offloader](https://github.com/mattermost/calls-offloader/releases) (required for recording, transcription, and live captions) and [mattermost-rtcd](https://github.com/mattermost/rtcd/releases) (required for performance and scalability).
- [Elasticsearch](https://www.elastic.co/downloads/elasticsearch) can be [deployed](https://www.elastic.co/docs/deploy-manage/deploy/self-managed/installing-elasticsearch) for enhanced search performance at scale.
- [Prometheus](https://prometheus.io/download/) and [Grafana](https://grafana.com/grafana/download) for monitoring and observability.
@@ -53,7 +53,7 @@ Kubernetes is recommended for a highly scalable and robust deployment if your or
Consider downloading these additional resources if you plan to enable these optional components:
-- [Mattermost Calls](/administration-guide/configure/calls-deployment-guide) helm charts: [mattermost-calls-offloader](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-calls-offloader) and [values](https://github.com/mattermost/mattermost-helm/blob/master/charts/mattermost-calls-offloader/values.yaml) (required for recording, transcription, and live captions), [mattermost-rtcd](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-rtcd) and [values](https://github.com/mattermost/mattermost-helm/blob/master/charts/mattermost-rtcd/values.yaml) (required for performance and scalability).
+- [Mattermost Calls](/deployment-guide/calls/calls-deployment-guide) helm charts: [mattermost-calls-offloader](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-calls-offloader) and [values](https://github.com/mattermost/mattermost-helm/blob/master/charts/mattermost-calls-offloader/values.yaml) (required for recording, transcription, and live captions), [mattermost-rtcd](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-rtcd) and [values](https://github.com/mattermost/mattermost-helm/blob/master/charts/mattermost-rtcd/values.yaml) (required for performance and scalability).
- [Elasticsearch](https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s) can be [deployed in air-gapped k8s environments](https://www.elastic.co/guide/en/cloud-on-k8s/2.8/k8s-air-gapped.html) for enhanced search performance at scale.
- [Prometheus](https://github.com/prometheus-operator/prometheus-operator) and [Grafana](https://github.com/grafana/grafana-operator) operators for monitoring and observability.
@@ -73,7 +73,7 @@ Docker can be used if you don't have a running Kubernetes cluster in the air-gap
Consider downloading these additional resources if you plan to enable these optional components:
-- [Mattermost Calls](/administration-guide/configure/calls-deployment-guide) images: [calls-offloader](https://hub.docker.com/r/mattermost/calls-offloader) (required for recording, transcription, and live captions) and [rtcd](https://hub.docker.com/r/mattermost/rtcd) (required for performance and scalability).
+- [Mattermost Calls](/deployment-guide/calls/calls-deployment-guide) images: [calls-offloader](https://hub.docker.com/r/mattermost/calls-offloader) (required for recording, transcription, and live captions) and [rtcd](https://hub.docker.com/r/mattermost/rtcd) (required for performance and scalability).
- [Elasticsearch](https://hub.docker.com/_/elasticsearch) image for enhanced search performance at scale.
- [Prometheus](https://hub.docker.com/r/prom/prometheus) and [Grafana](https://hub.docker.com/r/grafana/grafana) images for monitoring and observability.
diff --git a/docs/main/administration-guide/configure/calls-deployment-guide.mdx b/docs/main/deployment-guide/calls/calls-deployment-guide.mdx
similarity index 99%
rename from docs/main/administration-guide/configure/calls-deployment-guide.mdx
rename to docs/main/deployment-guide/calls/calls-deployment-guide.mdx
index baef51caf0b1..5912c9fe4838 100644
--- a/docs/main/administration-guide/configure/calls-deployment-guide.mdx
+++ b/docs/main/deployment-guide/calls/calls-deployment-guide.mdx
@@ -155,7 +155,7 @@ RTCD is a dedicated real-time communications service for Mattermost Calls that p
To determine if you'll need RTCD, start by answering the following questions about your deployment:
- **Are you deploying Calls on Kubernetes?**
- - **Yes:** You'll need to deploy RTCD as it's the only supported way to run Calls. See the [Kubernetes](/../../administration-guide/configure/calls-kubernetes) Calls deployment guide for details.
+ - **Yes:** You'll need to deploy RTCD as it's the only supported way to run Calls. See the [Kubernetes](calls-kubernetes.mdx) Calls deployment guide for details.
- **No:** Continue to the next question.
- **What is the *Total User* count of your existing Mattermost deployment?** _(Check [Site Statistics](/../../administration-guide/manage/statistics))_
- **Up to 50:** You can use the **Integrated** deployment model.
diff --git a/docs/main/administration-guide/configure/calls-kubernetes.mdx b/docs/main/deployment-guide/calls/calls-kubernetes.mdx
similarity index 87%
rename from docs/main/administration-guide/configure/calls-kubernetes.mdx
rename to docs/main/deployment-guide/calls/calls-kubernetes.mdx
index 5a6da8de2210..58a8fd6f9721 100644
--- a/docs/main/administration-guide/configure/calls-kubernetes.mdx
+++ b/docs/main/deployment-guide/calls/calls-kubernetes.mdx
@@ -65,7 +65,7 @@ For Kubernetes deployments, you need to ensure specific connectivity paths:
Resource requirements for RTCD pods depend heavily on the expected call volume, participant count, and whether screen sharing is used.
-We strongly recommend reviewing the [Performance Baselines](/administration-guide/configure/calls-metrics-monitoring#performance-baselines) to determine the appropriate CPU, memory, and network requests and limits for your specific deployment needs rather than relying on generic defaults.
+We strongly recommend reviewing the [Performance Baselines](calls-metrics-monitoring.mdx#performance-baselines) to determine the appropriate CPU, memory, and network requests and limits for your specific deployment needs rather than relying on generic defaults.
### Scaling Considerations
@@ -86,7 +86,7 @@ The general recommendation is to expose one external IP address per `rtcd` insta
## Monitoring and Metrics
-For detailed information on metrics collection and monitoring, see the [Calls Metrics and Monitoring](/administration-guide/configure/calls-metrics-monitoring) guide.
+For detailed information on metrics collection and monitoring, see the [Calls Metrics and Monitoring](calls-metrics-monitoring.mdx) guide.
## Troubleshooting
@@ -102,7 +102,7 @@ For detailed logging guidance, see the [Calls Logging](calls-logging.mdx) guide.
## Other Calls Documentation
- [Calls Deployment Guide](calls-deployment-guide.mdx): Overview of deployment options and architecture
-- [RTCD Setup and Configuration](/administration-guide/configure/calls-rtcd-setup): Comprehensive guide for setting up the dedicated RTCD service
+- [RTCD Setup and Configuration](calls-rtcd-setup.mdx): Comprehensive guide for setting up the dedicated RTCD service
- [Calls Offloader Setup and Configuration](calls-offloader-setup.mdx): Setup guide for call recording and transcription
-- [Calls Metrics and Monitoring](/administration-guide/configure/calls-metrics-monitoring): Guide to monitoring Calls performance using metrics and observability
+- [Calls Metrics and Monitoring](calls-metrics-monitoring.mdx): Guide to monitoring Calls performance using metrics and observability
- [Calls Logging](calls-logging.mdx): Detailed guidance for collecting Calls logs and client diagnostics
diff --git a/docs/main/administration-guide/configure/calls-logging.mdx b/docs/main/deployment-guide/calls/calls-logging.mdx
similarity index 100%
rename from docs/main/administration-guide/configure/calls-logging.mdx
rename to docs/main/deployment-guide/calls/calls-logging.mdx
diff --git a/docs/main/administration-guide/configure/calls-metrics-monitoring.mdx b/docs/main/deployment-guide/calls/calls-metrics-monitoring.mdx
similarity index 93%
rename from docs/main/administration-guide/configure/calls-metrics-monitoring.mdx
rename to docs/main/deployment-guide/calls/calls-metrics-monitoring.mdx
index e2eb5d2075e4..25659eed423a 100644
--- a/docs/main/administration-guide/configure/calls-metrics-monitoring.mdx
+++ b/docs/main/deployment-guide/calls/calls-metrics-monitoring.mdx
@@ -28,12 +28,12 @@ The metrics are exposed through HTTP endpoints:
Resource utilization metrics (CPU, memory, network) are mainly provided by an external service ([`node-exporter`](https://prometheus.io/docs/guides/node-exporter/)).
-> Metrics for the calls plugin are exposed through the `/plugins/com.mattermost.calls/metrics` subpath under the existing Mattermost server metrics endpoint. This is controlled by the [Listen address for performance](https://docs.mattermost.com/configure/environment-configuration-settings.html#listen-address-for-performance) configuration setting. It defaults to port 8067. For example: `http://localhost:8067/plugins/com.mattermost.calls/metrics`
+> Metrics for the calls plugin are exposed through the `/plugins/com.mattermost.calls/metrics` subpath under the existing Mattermost server metrics endpoint. This is controlled by the [Listen address for performance](/administration-guide/configure/environment-configuration-settings#listen-address-for-performance) configuration setting. It defaults to port 8067. For example: `http://localhost:8067/plugins/com.mattermost.calls/metrics`
> The RTCD Service `/metrics` endpoint is exposed on the HTTP API (e.g. `http://localhost:8045/metrics`).
## Setting Up Monitoring
-For instructions on deploying Prometheus and Grafana for Mattermost, please refer to the [Deploy Prometheus and Grafana for Performance Monitoring](https://docs.mattermost.com/scale/deploy-prometheus-grafana-for-performance-monitoring.html) guide.
+For instructions on deploying Prometheus and Grafana for Mattermost, please refer to the [Deploy Prometheus and Grafana for Performance Monitoring](/deployment-guide/reference-architecture/scale/deploy-prometheus-grafana-for-performance-monitoring) guide.
Once Prometheus and Grafana are set up, you will need to configure Prometheus to scrape metrics from the Calls-related services.
@@ -116,9 +116,9 @@ These metrics help monitor the health and resource usage of the RTCD process:
These metrics track the WebRTC connections and media flow:
-- `rtcd_rtc_conn_states_total{state="X"}`: Count of connections in different states
-- `rtcd_rtc_errors_total{type="X"}`: Count of RTC errors by type
-- `rtcd_rtc_rtp_tracks_total{direction="X"}`: Count of RTP tracks (incoming/outgoing)
+- `rtcd_rtc_conn_states_total{state="X"}`: Count of connections in different states
+- `rtcd_rtc_errors_total{type="X"}`: Count of RTC errors by type
+- `rtcd_rtc_rtp_tracks_total{direction="X"}`: Count of RTP tracks (incoming/outgoing)
- `rtcd_rtc_sessions_total`: Total number of active RTC sessions
#### WebSocket Metrics
@@ -126,7 +126,7 @@ These metrics track the WebRTC connections and media flow:
These metrics track the signaling channel:
- `rtcd_ws_connections_total`: Total number of active WebSocket connections. This is about RTCD <-> MM, so the connection count should match the number of MM nodes.
-- `rtcd_ws_messages_total{direction="X"}`: Count of WebSocket messages (sent/received)
+- `rtcd_ws_messages_total{direction="X"}`: Count of WebSocket messages (sent/received)
### Calls Plugin Metrics
diff --git a/docs/main/administration-guide/configure/calls-offloader-setup.mdx b/docs/main/deployment-guide/calls/calls-offloader-setup.mdx
similarity index 97%
rename from docs/main/administration-guide/configure/calls-offloader-setup.mdx
rename to docs/main/deployment-guide/calls/calls-offloader-setup.mdx
index f050232e336f..cb72d8f56c85 100644
--- a/docs/main/administration-guide/configure/calls-offloader-setup.mdx
+++ b/docs/main/deployment-guide/calls/calls-offloader-setup.mdx
@@ -371,7 +371,7 @@ tail -f /opt/calls-offloader/calls-offloader.log
### Performance Monitoring
-Monitor calls-offloader performance and resource usage to ensure optimal operation. See [Calls Metrics and Monitoring](/administration-guide/configure/calls-metrics-monitoring) for details on setting up metrics and observability.
+Monitor calls-offloader performance and resource usage to ensure optimal operation. See [Calls Metrics and Monitoring](calls-metrics-monitoring.mdx) for details on setting up metrics and observability.
## Air-Gapped Installation of `calls-offloader`
@@ -497,8 +497,8 @@ The `--image-registry` flag sets the registry prefix used when the offloader pul
## Other Calls Documentation
- [Calls Deployment Guide](calls-deployment-guide.mdx): Overview of deployment options and architecture
-- [RTCD Setup and Configuration](/administration-guide/configure/calls-rtcd-setup): Comprehensive guide for setting up the dedicated RTCD service
-- [Calls Metrics and Monitoring](/administration-guide/configure/calls-metrics-monitoring): Guide to monitoring Calls performance using metrics and observability
+- [RTCD Setup and Configuration](calls-rtcd-setup.mdx): Comprehensive guide for setting up the dedicated RTCD service
+- [Calls Metrics and Monitoring](calls-metrics-monitoring.mdx): Guide to monitoring Calls performance using metrics and observability
- [Calls Deployment on Kubernetes](calls-kubernetes.mdx): Detailed guide for deploying Calls in Kubernetes environments
- [Calls Logging](calls-logging.mdx): Detailed guidance for collecting Calls logs and client diagnostics
- [calls-offloader performance documentation](https://github.com/mattermost/calls-offloader/blob/master/docs/performance.md): Detailed performance tuning and monitoring recommendations
diff --git a/docs/main/administration-guide/configure/calls-rtcd-setup.mdx b/docs/main/deployment-guide/calls/calls-rtcd-setup.mdx
similarity index 96%
rename from docs/main/administration-guide/configure/calls-rtcd-setup.mdx
rename to docs/main/deployment-guide/calls/calls-rtcd-setup.mdx
index 74ec3f796f1e..5b14fab38871 100644
--- a/docs/main/administration-guide/configure/calls-rtcd-setup.mdx
+++ b/docs/main/deployment-guide/calls/calls-rtcd-setup.mdx
@@ -94,7 +94,7 @@ The following network connectivity is required:
| UDP (outgoing) |
Mattermost Instance(s) (Calls plugin) or rtcd service |
Configured STUN servers |
-(Optional) To allow for either Calls plugin or rtcd service to discover their instance public IP. Only needed if configuring STUN/TURN servers. This requirement does not apply when manually setting an IP or hostname through the ICE Host Override config option. |
+(Optional) To allow for either Calls plugin or rtcd service to discover their instance public IP. Only needed if configuring STUN/TURN servers. This requirement does not apply when manually setting an IP or hostname through the ICE Host Override config option. |
@@ -107,6 +107,12 @@ There are multiple ways to deploy RTCD, depending on your environment. We recomm
This is the recommended deployment method for non-Kubernetes production environments, as it provides the best performance and operational control. For Kubernetes deployments, see the [Calls Deployment on Kubernetes](calls-kubernetes.mdx) guide.
+
+
+These community-maintained scripts aren't published or reviewed by Mattermost. Review the script contents yourself before running them, and confirm they're suitable for your environment.
+
+
+
Looking for an automated setup? Check out these community-maintained [Calls Installation Scripts](https://github.com/bgardner8008/calls-install-scripts) for quick provisioning of the RTCD service on Ubuntu/Debian systems.
@@ -130,9 +136,11 @@ Looking for an automated setup? Check out these community-maintained [Calls Inst
sudo mv rtcd-linux-amd64 /opt/rtcd/rtcd
```
- ```{note}
+
+
Replace `rtcd-linux-amd64` with the appropriate binary for your system architecture (e.g., `rtcd-linux-arm64` for ARM64 systems). The binary should be placed at `/opt/rtcd/rtcd` as this is the expected location referenced in systemd service files and other documentation.
- ```
+
+
2. **Create a configuration file** (`/opt/rtcd/rtcd.toml`):
@@ -200,9 +208,11 @@ Docker deployment is suitable for development, testing, or containerized product
mattermost/rtcd:latest
```
- ```{note}
+
+
If you optionally use the `RTCD_API_SECURITY_ALLOWSELFREGISTRATION` setting, please note that it defaults to `false`. If enabled, it allows anyone who can connect to the service on the API port (8045) to successfully initiate calls. Understand the security implications of this setting before enabling it.
- ```
+
+
2. For debugging purposes, you can enable more detailed logging:
diff --git a/docs/main/deployment-guide/deployment-architecture.mdx b/docs/main/deployment-guide/deployment-architecture.mdx
index 604cb06fb761..9091b0175999 100644
--- a/docs/main/deployment-guide/deployment-architecture.mdx
+++ b/docs/main/deployment-guide/deployment-architecture.mdx
@@ -22,7 +22,7 @@ Once you've sized the architecture:
- For **air-gapped** environments, also work through [Air-Gapped Operations](/deployment-guide/air-gapped-operations/quick-start-runbook). The architecture is the same; the install procedure and dependency-staging are different.
- For **regulated workloads** (DoD IL, DISA STIG), see [Security & Compliance → Compliance Frameworks](/security-guide/compliance-frameworks) for the configuration overlay that goes on top of the architecture choices above.
-- For **HA tuning** (clustering, Redis configuration, multi-AZ database topology), see [High Availability cluster-based deployment](/administration-guide/scale/high-availability-cluster-based-deployment).
+- For **HA tuning** (clustering, Redis configuration, multi-AZ database topology), see [High Availability cluster-based deployment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment).
- For **specific install paths**, jump to [Install on Linux](/deployment-guide/server/deploy-linux), [Install on Kubernetes](/deployment-guide/server/deploy-kubernetes), or [Install with Containers](/deployment-guide/server/deploy-containers).
:::note[Want the planning context first?]
diff --git a/docs/main/deployment-guide/deployment-guide-index.mdx b/docs/main/deployment-guide/deployment-guide-index.mdx
index fbd0ca13de93..f30ed322eeb8 100644
--- a/docs/main/deployment-guide/deployment-guide-index.mdx
+++ b/docs/main/deployment-guide/deployment-guide-index.mdx
@@ -8,7 +8,7 @@ Whether you're deploying the server application, desktop application, or mobile
- [Quick Start Evaluation](/deployment-guide/quick-start-evaluation) - A quick start guide to help you get started with Mattermost.
- [Reference Architecture](/deployment-guide/reference-architecture/reference-architecture-index) - Reference architectures for scaling Mattermost and specialized deployment scenarios.
- [Server deployment](/deployment-guide/server/server-deployment-planning) - Pre-deployment checks, security considerations, hardware recommendations, software prerequisites, and step-by-step guidance to deploying Mattermost server.
-- [Calls Deployment Guide](/administration-guide/configure/calls-deployment-guide) - Deploy and operate Mattermost Calls infrastructure, including RTCD, recording services, and supporting network configuration.
+- [Calls Deployment Guide](/deployment-guide/calls/calls-deployment-guide) - Deploy and operate Mattermost Calls infrastructure, including RTCD, recording services, and supporting network configuration.
- [Desktop deployment](/deployment-guide/desktop/desktop-app-deployment) - Installation procedures for Mattermost's desktop applications across Windows, macOS, and Linux, and large-scale, enterprise-wide deployments.
- [Mobile deployment](/deployment-guide/mobile/mobile-app-deployment) - How to set up push notifications using Mattermost's notification service and troubleshooting tips.
- [Deployment troubleshooting](/deployment-guide/deployment-troubleshooting) - Best practices for diagnosing and resolving common deployment issues.
diff --git a/docs/main/deployment-guide/reference-architecture/deployment-scenarios/air-gapped-deployment.mdx b/docs/main/deployment-guide/reference-architecture/deployment-scenarios/air-gapped-deployment.mdx
index 7fa15f7bcccb..c639831613bf 100644
--- a/docs/main/deployment-guide/reference-architecture/deployment-scenarios/air-gapped-deployment.mdx
+++ b/docs/main/deployment-guide/reference-architecture/deployment-scenarios/air-gapped-deployment.mdx
@@ -63,7 +63,7 @@ Using our provided tarball is recommeded as the simplest installation method for
>
> **(Optional) Supporting Services** Consider downloading these additional resources if you plan to enable these optional components:
>
-> - [Mattermost Calls](/administration-guide/configure/calls-deployment-guide): [mattermost-calls-offloader](https://github.com/mattermost/calls-offloader/releases) (required for recording, transcription and live captions) and [mattermost-rtcd](https://github.com/mattermost/rtcd/releases) (required for performance and scalability).
+> - [Mattermost Calls](/deployment-guide/calls/calls-deployment-guide): [mattermost-calls-offloader](https://github.com/mattermost/calls-offloader/releases) (required for recording, transcription and live captions) and [mattermost-rtcd](https://github.com/mattermost/rtcd/releases) (required for performance and scalability).
> - [Elasticsearch](https://www.elastic.co/downloads/elasticsearch) can be [deployed](https://www.elastic.co/docs/deploy-manage/deploy/self-managed/installing-elasticsearch) for enhanced search performance at scale.
> - [Prometheus](https://prometheus.io/download/) and [Grafana](https://grafana.com/grafana/download) for monitoring and observability
@@ -106,7 +106,7 @@ Kubernetes is recommended for a highly scalable and robust deployment if your or
>
> **(Optional) Supporting Services** Consider downloading these additional resources if you plan to enable these optional components:
>
-> - [Mattermost Calls](/administration-guide/configure/calls-deployment-guide) helm charts: [mattermost-calls-offloader](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-calls-offloader) and [values](https://github.com/mattermost/mattermost-helm/blob/master/charts/mattermost-calls-offloader/values.yaml) (required for recording, transcription and live captions), [mattermost-rtcd](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-rtcd) and [values](https://github.com/mattermost/mattermost-helm/blob/master/charts/mattermost-rtcd/values.yaml) (required for performance and scalability).
+> - [Mattermost Calls](/deployment-guide/calls/calls-deployment-guide) helm charts: [mattermost-calls-offloader](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-calls-offloader) and [values](https://github.com/mattermost/mattermost-helm/blob/master/charts/mattermost-calls-offloader/values.yaml) (required for recording, transcription and live captions), [mattermost-rtcd](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-rtcd) and [values](https://github.com/mattermost/mattermost-helm/blob/master/charts/mattermost-rtcd/values.yaml) (required for performance and scalability).
> - [Elasticsearch](https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s) can be [deployed in air-gapped k8 environments](https://www.elastic.co/guide/en/cloud-on-k8s/2.8/k8s-air-gapped.html) for enhanced search performance at scale.
> - [Prometheus](https://github.com/prometheus-operator/prometheus-operator) and [Grafana](https://github.com/grafana/grafana-operator) operators for monitoring and observability
@@ -124,7 +124,7 @@ Docker can be used if you don't have a running Kubernetes cluster in the air-gap
>
> **(Optional) Supporting Services** Consider downloading these additional resources if you plan to enable these optional components:
>
-> - [Mattermost Calls](/administration-guide/configure/calls-deployment-guide) images: [calls-offloader](https://hub.docker.com/r/mattermost/calls-offloader) (required for recording, transcription and live captions) and [rtcd](https://hub.docker.com/r/mattermost/rtcd) (required for performance and scalability).
+> - [Mattermost Calls](/deployment-guide/calls/calls-deployment-guide) images: [calls-offloader](https://hub.docker.com/r/mattermost/calls-offloader) (required for recording, transcription and live captions) and [rtcd](https://hub.docker.com/r/mattermost/rtcd) (required for performance and scalability).
> - [Elasticsearch](https://hub.docker.com/_/elasticsearch) image for enhanced search performance at scale.
> - [Prometheus](https://hub.docker.com/r/prom/prometheus) and [Grafana](https://hub.docker.com/r/grafana/grafana) images for monitoring and observability.
diff --git a/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-ddil-operations.mdx b/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-ddil-operations.mdx
index fc8357e0f359..6d22131d7bfc 100644
--- a/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-ddil-operations.mdx
+++ b/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-ddil-operations.mdx
@@ -36,17 +36,17 @@ This hybrid deployment architecture provides optimal collaboration in both conne
- When internet disconnected: Microsoft services are unreachable, but embedded Mattermost application remains fully operational for tactical teams.
- **Mattermost Desktop Apps:** Access Mattermost via [desktop](/deployment-guide/desktop/desktop-app-deployment) or web apps in addition to the embedded views from Teams and Outlook. *(Optional - not shown)*
- **Mattermost Mobile Apps:** Access Mattermost via [iPhone and Android apps](/deployment-guide/mobile/mobile-app-deployment), with support for [ID-only push notifications](/deployment-guide/mobile/host-your-own-push-proxy-service) to ensure compliance with data sovereignty requirements. *(Optional when connectivity permits - not shown)*
-- **Mattermost Deployment:** Mattermost deployed for sovereign tactical collaboration on local infrastructure, such as [Azure Local](https://learn.microsoft.com/en-us/azure/azure-local/manage/disconnected-operations-overview), supporting data residency regulations and [disconnected operations](/deployment-guide/reference-architecture/deployment-scenarios/air-gapped-deployment). See [reference architecture](/administration-guide/scale/server-architecture) documentation for Mattermost deployment configurations based on expected scale.
+- **Mattermost Deployment:** Mattermost deployed for sovereign tactical collaboration on local infrastructure, such as [Azure Local](https://learn.microsoft.com/en-us/azure/azure-local/manage/disconnected-operations-overview), supporting data residency regulations and [disconnected operations](/deployment-guide/reference-architecture/deployment-scenarios/air-gapped-deployment). See [reference architecture](/deployment-guide/reference-architecture/scale/server-architecture) documentation for Mattermost deployment configurations based on expected scale.
- **Mattermost Server:** Core application server handling tactical collaboration workloads, including:
- - [Messaging Collaboration](/end-user-guide/messaging-collaboration): DDIL-ready 1:1, group messaging, and structured channel collaboration with [rich integration capabilities](/integrations-guide/integrations-guide-index) and [enterprise-grade search](/administration-guide/scale/scaling-for-enterprise#enterprise-search).
+ - [Messaging Collaboration](/end-user-guide/messaging-collaboration): DDIL-ready 1:1, group messaging, and structured channel collaboration with [rich integration capabilities](/integrations-guide/integrations-guide-index) and [enterprise-grade search](/deployment-guide/reference-architecture/scale/scaling-for-enterprise#enterprise-search).
- [Workflow Automation](/end-user-guide/workflow-automation): Playbooks provide structure, monitoring and automation for repeatable processes built-in to your local Mattermost deployment.
- [Project Tracking](/end-user-guide/project-task-management): Boards enables project management capabilities built-in to your local Mattermost deployment.
- [AI Agents](/administration-guide/configure/agents-admin-guide): AI Agents run against a local LLM hosted within your tactical network.
- - [Audio & Screenshare](/administration-guide/configure/calls-deployment-guide): Calls offers native real-time self-hosted audio calls and screen sharing within your tactical network.
+ - [Audio & Screenshare](/deployment-guide/calls/calls-deployment-guide): Calls offers native real-time self-hosted audio calls and screen sharing within your tactical network.
- **Proxy Server:** The [proxy server](/deployment-guide/server/setup-nginx-proxy) handles HTTP(S) routing within the cluster, directing traffic between the server and clients accessing Mattermost services. NGINX is recommended for load balancing with support for WebSocket connections, health check endpoints, and sticky sessions. The proxy layer provides SSL termination and distributes client traffic across application servers.
- **PostgreSQL Database:** Stores persistent application data on a [PostgreSQL v13+ database](/deployment-guide/server/preparations) hosted locally within your tactical network.
- **Object Storage:** File uploads, images, and attachments are stored outside the application node on an [S3-compatible store](/deployment-guide/server/preparations) or network/local storage, hosted locally within your tactical network.
- - **Recording Instance:** `calls-offloader` job service, configured using the [Calls Offloader Setup and Configuration](/administration-guide/configure/calls-offloader-setup) guide, to offload heavy processing tasks from Mattermost Calls to self-hosted infrastructure within your tactical network, such as recordings, transcriptions, and live captioning. *(Optional)*
+ - **Recording Instance:** `calls-offloader` job service, configured using the [Calls Offloader Setup and Configuration](/deployment-guide/calls/calls-offloader-setup) guide, to offload heavy processing tasks from Mattermost Calls to self-hosted infrastructure within your tactical network, such as recordings, transcriptions, and live captioning. *(Optional)*
- **Self-hosted integrations:** [Custom apps, plugins, and webhooks](/integrations-guide/integrations-guide-index) can be deployed within your tactical network. *(Optional - not shown)*
- **Self-hosted LLM:** Locally hosted [OpenAI compatible LLM](/agents/docs/providers) for agentic powered collaboration within your tactical network. *(Optional)*
- **Microsoft Global Network:** [World-wide network](https://learn.microsoft.com/en-us/azure/networking/microsoft-global-network) of Microsoft data centers, delivering public cloud services when internet connectivity permits.
@@ -70,17 +70,17 @@ Deploy an [OpenAI compatible LLM](/administration-guide/configure/agents-admin-g
### Self-hosted audio & screensharing
-Effective collaboration at the tactical edge requires all voice and screen sharing capabilities remain operational without reliance on the internet or third-party services. Deploy [Mattermost Calls](/administration-guide/configure/calls-deployment-guide) in a self-hosted configuration, including:
+Effective collaboration at the tactical edge requires all voice and screen sharing capabilities remain operational without reliance on the internet or third-party services. Deploy [Mattermost Calls](/deployment-guide/calls/calls-deployment-guide) in a self-hosted configuration, including:
-- The `rtcd` service, configured using the [RTCD Setup and Configuration](/administration-guide/configure/calls-rtcd-setup) guide, provides scalable, low-latency media routing hosted on-premises. Run multiple `rtcd` nodes for redundancy.
-- The `calls-offloader` service, configured using the [Calls Offloader Setup and Configuration](/administration-guide/configure/calls-offloader-setup) guide, offloads heavy processing tasks like recording, transcription and live captioning to a locally hosted compliance-approved job server.
+- The `rtcd` service, configured using the [RTCD Setup and Configuration](/deployment-guide/calls/calls-rtcd-setup) guide, provides scalable, low-latency media routing hosted on-premises. Run multiple `rtcd` nodes for redundancy.
+- The `calls-offloader` service, configured using the [Calls Offloader Setup and Configuration](/deployment-guide/calls/calls-offloader-setup) guide, offloads heavy processing tasks like recording, transcription and live captioning to a locally hosted compliance-approved job server.
### High availability and fault tolerance
-Deploy Mattermost in a [cluster-based architecture](/administration-guide/scale/high-availability-cluster-based-deployment) to ensure continued availability during outages or hardware failures. High availability requires redundant infrastructure across each critical component:
+Deploy Mattermost in a [cluster-based architecture](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) to ensure continued availability during outages or hardware failures. High availability requires redundant infrastructure across each critical component:
- Application servers: Scale horizontally across multiple nodes with a load balancer distributing client traffic.
-- Search service: [Elasticsearch or AWS OpenSearch Service](/administration-guide/scale/scaling-for-enterprise#enterprise-search) provides optimized search performance with dedicated indexing for large-scale deployments.
+- Search service: [Elasticsearch or AWS OpenSearch Service](/deployment-guide/reference-architecture/scale/scaling-for-enterprise#enterprise-search) provides optimized search performance with dedicated indexing for large-scale deployments.
- Object storage: Configure S3-compatible backends with erasure coding or replication for durability. All application servers must access shared file storage (NAS or S3) to ensure consistent data availability.
- Calls services: Run multiple `rtcd` and offloader nodes for resilience.
diff --git a/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-mission-partner.mdx b/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-mission-partner.mdx
index 6c192e809972..8a367455b5ed 100644
--- a/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-mission-partner.mdx
+++ b/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-mission-partner.mdx
@@ -52,17 +52,17 @@ The deployment architecture includes the following components:
- **Mattermost Desktop Apps:** Access Mattermost directly by deploying [desktop](/deployment-guide/desktop/desktop-app-deployment) or web apps in your organization.
- **Mattermost Mobile Apps:** Access Mattermost via [iPhone and Android apps](/deployment-guide/mobile/mobile-app-deployment), with support for [ID-only push notifications](/deployment-guide/mobile/host-your-own-push-proxy-service) to ensure compliance with data sovereignty requirements. *(Optional - not shown)*
- **Microsoft 365 Desktop Apps:** For partnered organizations using Microsoft 365 services, Teams and Outlook can be deployed with the [embedded Mattermost application](/integrations-guide/mattermost-mission-collaboration-for-m365) for cross-domain partner collaboration within a familiar interface. *(Optional)*
-- **Mattermost Deployments:** Mattermost deployed for sovereign collaboration on private cloud or local infrastructure, such as [Azure](/deployment-guide/server/deploy-kubernetes) or [Azure Local](https://learn.microsoft.com/en-us/azure/azure-local/manage/disconnected-operations-overview), to maintain compliance with STIG, FedRAMP, and NIST 800-53 standards. See [reference architecture](/administration-guide/scale/server-architecture) documentation for Mattermost deployment configurations based on expected scale.
+- **Mattermost Deployments:** Mattermost deployed for sovereign collaboration on private cloud or local infrastructure, such as [Azure](/deployment-guide/server/deploy-kubernetes) or [Azure Local](https://learn.microsoft.com/en-us/azure/azure-local/manage/disconnected-operations-overview), to maintain compliance with STIG, FedRAMP, and NIST 800-53 standards. See [reference architecture](/deployment-guide/reference-architecture/scale/server-architecture) documentation for Mattermost deployment configurations based on expected scale.
- **Mattermost Server:** Core application server handling collaboration workloads, including:
- [Messaging Collaboration](/end-user-guide/messaging-collaboration): Sovereign 1:1, group messaging, and structured channel collaboration.
- [Workflow Automation](/end-user-guide/workflow-automation): Playbooks provide structure, monitoring and automation for repeatable processes built-in to your sovereign Mattermost deployment.
- [Project Tracking](/end-user-guide/project-task-management): Boards enables project management capabilities built-in to your local Mattermost deployment. Boards enables project management capabilities built-in to your sovereign Mattermost deployment.
- [AI Agents](/administration-guide/configure/agents-admin-guide): AI Agents run against Azure OpenAI endpoints or a self-hosted LLM that is OpenAI-compatible.
- - [Audio & Screenshare](/administration-guide/configure/calls-deployment-guide): Calls offers native real-time self-hosted audio calls and screen sharing within your own network.
+ - [Audio & Screenshare](/deployment-guide/calls/calls-deployment-guide): Calls offers native real-time self-hosted audio calls and screen sharing within your own network.
- **Proxy Server:** The [proxy server](/deployment-guide/server/setup-nginx-proxy) handles HTTP(S) routing within the cluster, directing traffic between the server and clients accessing Mattermost services, including requests from users in [connected organizations](/administration-guide/onboard/connected-workspaces). NGINX is recommended for load balancing with support for WebSocket connections, health check endpoints, and sticky sessions. The proxy layer provides SSL termination and distributes client traffic across application servers.
- **PostgreSQL Database:** Stores persistent application data on a [PostgreSQL v13+ database](/deployment-guide/server/preparations), such as [Azure Database for PostgreSQL](https://azure.microsoft.com/en-us/products/postgresql).
- **Object Storage:** File uploads, images, and attachments are stored outside the application node on an [S3-compatible store](/deployment-guide/server/preparations) or an NFS (Network File System) server. [Azure Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs) can be used, but needs an S3-compatible proxy for Mattermost to interface with.
- - **Recording Instance:** `calls-offloader` job service, configured using the [Calls Offloader Setup and Configuration](/administration-guide/configure/calls-offloader-setup) guide, to offload heavy processing tasks from Mattermost Calls, such as recordings, transcriptions, and live captioning, to local infrastructure or private cloud. *(Optional)*
+ - **Recording Instance:** `calls-offloader` job service, configured using the [Calls Offloader Setup and Configuration](/deployment-guide/calls/calls-offloader-setup) guide, to offload heavy processing tasks from Mattermost Calls, such as recordings, transcriptions, and live captioning, to local infrastructure or private cloud. *(Optional)*
- **Integration framework:** [Custom apps, plugins, and webhooks](/integrations-guide/integrations-guide-index) can be deployed for real-time data integrations and alerting. *(Optional - not shown)*
- **Self-hosted LLM:** Locally hosted [OpenAI compatible LLM](/agents/docs/providers) for agentic powered collaboration. *(Optional)*
- **Microsoft Global Network:** [World-wide network](https://learn.microsoft.com/en-us/azure/networking/microsoft-global-network) of Microsoft data centers, delivering public cloud services including M365 and Azure OpenAI. *(Optional)*
@@ -119,19 +119,19 @@ AI capabilities enhance mission collaboration with summarization, translation, s
### High availability and fault tolerance
-Deploy Mattermost in a [cluster-based architecture](/administration-guide/scale/high-availability-cluster-based-deployment) to ensure continued availability during outages or hardware failures. High availability requires redundant infrastructure across each critical component:
+Deploy Mattermost in a [cluster-based architecture](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) to ensure continued availability during outages or hardware failures. High availability requires redundant infrastructure across each critical component:
- Application servers: Scale horizontally across multiple nodes with a load balancer distributing client traffic.
-- Search service: [Elasticsearch or AWS OpenSearch Service](/administration-guide/scale/scaling-for-enterprise#enterprise-search) provides optimized search performance with dedicated indexing for large-scale deployments.
+- Search service: [Elasticsearch or AWS OpenSearch Service](/deployment-guide/reference-architecture/scale/scaling-for-enterprise#enterprise-search) provides optimized search performance with dedicated indexing for large-scale deployments.
- Object storage: Configure S3-compatible backends with erasure coding or replication for durability. All application servers must access shared file storage (NAS or S3) to ensure consistent data availability.
- Calls services: Run multiple `rtcd` and offloader nodes for resilience.
### Sovereign audio & screensharing
-Deploy [Mattermost Calls](/administration-guide/configure/calls-deployment-guide) in a self-hosted configuration to ensure voice and screen sharing capabilities remain operational without reliance on the internet, and that media traffic does not traverse non-compliant third-party services.
+Deploy [Mattermost Calls](/deployment-guide/calls/calls-deployment-guide) in a self-hosted configuration to ensure voice and screen sharing capabilities remain operational without reliance on the internet, and that media traffic does not traverse non-compliant third-party services.
-- The `rtcd` service, configured using the [RTCD Setup and Configuration](/administration-guide/configure/calls-rtcd-setup) guide, provides scalable, low-latency media routing hosted on-premises. Run multiple `rtcd` nodes for redundancy.
-- The `calls-offloader` service, configured using the [Calls Offloader Setup and Configuration](/administration-guide/configure/calls-offloader-setup) guide, offloads heavy processing tasks like recording, transcription and live captioning to a compliance-approved job server.
+- The `rtcd` service, configured using the [RTCD Setup and Configuration](/deployment-guide/calls/calls-rtcd-setup) guide, provides scalable, low-latency media routing hosted on-premises. Run multiple `rtcd` nodes for redundancy.
+- The `calls-offloader` service, configured using the [Calls Offloader Setup and Configuration](/deployment-guide/calls/calls-offloader-setup) guide, offloads heavy processing tasks like recording, transcription and live captioning to a compliance-approved job server.
### Compliance and retention
diff --git a/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-sovereign-collaboration.mdx b/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-sovereign-collaboration.mdx
index 8ecb1be4b20b..20a8673c25b7 100644
--- a/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-sovereign-collaboration.mdx
+++ b/docs/main/deployment-guide/reference-architecture/deployment-scenarios/deploy-sovereign-collaboration.mdx
@@ -31,17 +31,17 @@ The deployment architecture includes the following components:
- **Microsoft 365 Desktop Apps:** Teams and Outlook with [embedded Mattermost application](/integrations-guide/mattermost-mission-collaboration-for-m365) for seamless collaboration within a familiar interface while enforcing regulatory compliance.
- **Mattermost Desktop Apps:** Access Mattermost via [desktop](/deployment-guide/desktop/desktop-app-deployment) or web apps in addition to the embedded views from Teams and Outlook. *(Optional - not shown)*
- **Mattermost Mobile Apps:** Access Mattermost via [iPhone and Android apps](/deployment-guide/mobile/mobile-app-deployment), with support for [ID-only push notifications](/deployment-guide/mobile/host-your-own-push-proxy-service) to ensure compliance with data sovereignty requirements. *(Optional - not shown)*
-- **Mattermost Deployment:** Mattermost deployed for sovereign collaboration on enterprise-controlled infrastructure or private cloud, such as [Azure](/deployment-guide/server/deploy-kubernetes) or [Azure Local](https://learn.microsoft.com/en-us/azure/azure-local/manage/disconnected-operations-overview), to maintain compliance with STIG, FedRAMP, and NIST 800-53 standards. See [reference architecture](/administration-guide/scale/server-architecture) documentation for Mattermost deployment configurations based on expected scale.
+- **Mattermost Deployment:** Mattermost deployed for sovereign collaboration on enterprise-controlled infrastructure or private cloud, such as [Azure](/deployment-guide/server/deploy-kubernetes) or [Azure Local](https://learn.microsoft.com/en-us/azure/azure-local/manage/disconnected-operations-overview), to maintain compliance with STIG, FedRAMP, and NIST 800-53 standards. See [reference architecture](/deployment-guide/reference-architecture/scale/server-architecture) documentation for Mattermost deployment configurations based on expected scale.
- **Mattermost Server:** Core application server handling collaboration workloads, including:
- - [Messaging Collaboration](/end-user-guide/messaging-collaboration): DDIL-ready 1:1, group messaging, and structured channel collaboration with [rich integration capabilities](/integrations-guide/integrations-guide-index) and [enterprise-grade search](/administration-guide/scale/scaling-for-enterprise#enterprise-search).
+ - [Messaging Collaboration](/end-user-guide/messaging-collaboration): DDIL-ready 1:1, group messaging, and structured channel collaboration with [rich integration capabilities](/integrations-guide/integrations-guide-index) and [enterprise-grade search](/deployment-guide/reference-architecture/scale/scaling-for-enterprise#enterprise-search).
- [Workflow Automation](/end-user-guide/workflow-automation): Playbooks provide structure, monitoring and automation for repeatable processes built-in to your sovereign Mattermost deployment.
- [Project Tracking](/end-user-guide/project-task-management): Boards enables project management capabilities built-in to your sovereign Mattermost deployment.
- [AI Agents](/administration-guide/configure/agents-admin-guide): AI Agents run against Azure OpenAI endpoints or a self-hosted LLM that is OpenAI-compatible.
- - [Audio & Screenshare](/administration-guide/configure/calls-deployment-guide): Calls offers native real-time self-hosted audio calls and screen sharing within your own network.
+ - [Audio & Screenshare](/deployment-guide/calls/calls-deployment-guide): Calls offers native real-time self-hosted audio calls and screen sharing within your own network.
- **Proxy Server:** The [proxy server](/deployment-guide/server/setup-nginx-proxy) handles HTTP(S) routing within the cluster, directing traffic between the server and clients accessing Mattermost services. NGINX is recommended for load balancing with support for WebSocket connections, health check endpoints, and sticky sessions. The proxy layer provides SSL termination and distributes client traffic across application servers.
- **PostgreSQL Database:** Stores persistent application data on a [PostgreSQL v13+ database](/deployment-guide/server/preparations), such as [Azure Database for PostgreSQL](https://azure.microsoft.com/en-us/products/postgresql).
- **Object Storage:** File uploads, images, and attachments are stored outside the application node on an [S3-compatible store](/deployment-guide/server/preparations) or an NFS (Network File System) server. [Azure Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs) can be used, but needs an S3-compatible proxy for Mattermost to interface with.
- - **Recording Instance:** `calls-offloader` job service, configured using the [Calls Offloader Setup and Configuration](/administration-guide/configure/calls-offloader-setup) guide, to offload heavy processing tasks from Mattermost Calls, such as recordings, transcriptions, and live captioning, to enterprise-controlled infrastructure or private cloud. *(Optional)*
+ - **Recording Instance:** `calls-offloader` job service, configured using the [Calls Offloader Setup and Configuration](/deployment-guide/calls/calls-offloader-setup) guide, to offload heavy processing tasks from Mattermost Calls, such as recordings, transcriptions, and live captioning, to enterprise-controlled infrastructure or private cloud. *(Optional)*
- **Self-hosted integrations:** [Custom apps, plugins, and webhooks](/integrations-guide/integrations-guide-index) can be deployed within the enterprise boundary. *(Optional - not shown)*
**Secure Access Layer:** A firewall or access gateway protecting entry into the enterprise network. This may include network policies, IP allowlists, or WAFs depending on your networking configurations. *(Optional)*
@@ -56,20 +56,20 @@ The following best practices and deployment configurations help ensure that Matt
### High availability and fault tolerance
-Deploy Mattermost in a [cluster-based architecture](/administration-guide/scale/high-availability-cluster-based-deployment) to ensure continued availability during outages or hardware failures. High availability requires redundant infrastructure across each critical component:
+Deploy Mattermost in a [cluster-based architecture](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) to ensure continued availability during outages or hardware failures. High availability requires redundant infrastructure across each critical component:
- Application servers: Scale horizontally across multiple nodes with a load balancer distributing client traffic.
- Database layer: Use PostgreSQL replication or managed HA services with automatic failover.
-- Search service: [Elasticsearch or AWS OpenSearch Service](/administration-guide/scale/scaling-for-enterprise#enterprise-search) provides optimized search performance with dedicated indexing for large-scale deployments.
+- Search service: [Elasticsearch or AWS OpenSearch Service](/deployment-guide/reference-architecture/scale/scaling-for-enterprise#enterprise-search) provides optimized search performance with dedicated indexing for large-scale deployments.
- Object storage: Configure S3-compatible backends with erasure coding or replication for durability. All application servers must access shared file storage (NAS or S3) to ensure consistent data availability.
- Calls services: Run multiple `rtcd` and `calls-offloader` nodes for resilience.
### Sovereign audio & screensharing
-Data sovereignty compliance may require that all voice and screen sharing traffic remain within enterprise-controlled infrastructure and does not traverse third-party services. Deploy [Mattermost Calls](/administration-guide/configure/calls-deployment-guide) in a self-hosted configuration to ensure that Microsoft Teams users and Mattermost users collaborate without media ever leaving the sovereign network.
+Data sovereignty compliance may require that all voice and screen sharing traffic remain within enterprise-controlled infrastructure and does not traverse third-party services. Deploy [Mattermost Calls](/deployment-guide/calls/calls-deployment-guide) in a self-hosted configuration to ensure that Microsoft Teams users and Mattermost users collaborate without media ever leaving the sovereign network.
-- The `rtcd` service, configured using the [RTCD Setup and Configuration](/administration-guide/configure/calls-rtcd-setup) guide, provides scalable, low-latency media routing hosted on-premises. Run multiple `rtcd` nodes for redundancy.
-- The `calls-offloader` service, configured using the [Calls Offloader Setup and Configuration](/administration-guide/configure/calls-offloader-setup) guide, offloads heavy processing tasks like recording, transcription and live captioning to a compliance-approved job server.
+- The `rtcd` service, configured using the [RTCD Setup and Configuration](/deployment-guide/calls/calls-rtcd-setup) guide, provides scalable, low-latency media routing hosted on-premises. Run multiple `rtcd` nodes for redundancy.
+- The `calls-offloader` service, configured using the [Calls Offloader Setup and Configuration](/deployment-guide/calls/calls-offloader-setup) guide, offloads heavy processing tasks like recording, transcription and live captioning to a compliance-approved job server.
### Compliance and retention
diff --git a/docs/main/deployment-guide/reference-architecture/reference-architecture-index.mdx b/docs/main/deployment-guide/reference-architecture/reference-architecture-index.mdx
index 133c0c28b5ce..08307ffb80d2 100644
--- a/docs/main/deployment-guide/reference-architecture/reference-architecture-index.mdx
+++ b/docs/main/deployment-guide/reference-architecture/reference-architecture-index.mdx
@@ -4,5 +4,5 @@ title: "Reference Architecture"
Mattermost reference architectures describe recommended deployment patterns and system designs that ensure secure, scalable, and resilient collaboration. These frameworks serve as a guide for implementing Mattermost at scale across a range of operational scenarios.
- [Application Architecture](/deployment-guide/reference-architecture/application-architecture) - An overview of Mattermost's architecture and components.
-- [Scaling Architecture](/administration-guide/scale/scaling-for-enterprise) - Reference architecture for high availability, clustering, and enterprise-scale deployments.
+- [Scaling Architecture](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) - Reference architecture for high availability, clustering, and enterprise-scale deployments.
- [Deployment Scenarios](/deployment-guide/reference-architecture/deployment-scenarios/deployment-scenarios-index) - Reference architectures tailored for specialized Mattermost deployment scenarios.
diff --git a/docs/main/administration-guide/scale/additional-ha-considerations.mdx b/docs/main/deployment-guide/reference-architecture/scale/additional-ha-considerations.mdx
similarity index 51%
rename from docs/main/administration-guide/scale/additional-ha-considerations.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/additional-ha-considerations.mdx
index f543a68c4910..44dd9222e36d 100644
--- a/docs/main/administration-guide/scale/additional-ha-considerations.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/additional-ha-considerations.mdx
@@ -2,6 +2,6 @@
title: Additional HA considerations
unlisted: true
---
-[Elasticsearch](https://www.elastic.co) provides enterprise-scale deployments with optimized search performance and prevents performance degradation and timeouts. Elasticsearch allows you to search large volumes of data quickly, in near real-time, by creating and managing an index of post data. Mattermost’s implementation uses [Elasticsearch](https://www.elastic.co) as a distributed, RESTful search engine supporting highly efficient database searches in a [cluster environment](/administration-guide/scale/high-availability-cluster-based-deployment). Visit the [Mattermost Elasticsearch product documentation](/administration-guide/scale/elasticsearch-setup) for deployment and configuration details.
+[Elasticsearch](https://www.elastic.co) provides enterprise-scale deployments with optimized search performance and prevents performance degradation and timeouts. Elasticsearch allows you to search large volumes of data quickly, in near real-time, by creating and managing an index of post data. Mattermost’s implementation uses [Elasticsearch](https://www.elastic.co) as a distributed, RESTful search engine supporting highly efficient database searches in a [cluster environment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment). Visit the [Mattermost Elasticsearch product documentation](/deployment-guide/reference-architecture/scale/elasticsearch-setup) for deployment and configuration details.
-Performance monitoring support enables a Mattermost server to track system health for large Enterprise deployments through integrations with [Prometheus](https://prometheus.io/) and [Grafana](https://grafana.com/). These integrations support data collection from several Mattermost servers, which is particularly useful if you’re running Mattermost [in high availability mode](/administration-guide/scale/high-availability-cluster-based-deployment). Once you’re tracking system health, you can [set up performance alerts](/administration-guide/scale/performance-alerting) on your Grafana dashboard. Visit the [Mattermost Performance Monitoring product documentation](/administration-guide/scale/deploy-prometheus-grafana-for-performance-monitoring) for installation details.
+Performance monitoring support enables a Mattermost server to track system health for large Enterprise deployments through integrations with [Prometheus](https://prometheus.io/) and [Grafana](https://grafana.com/). These integrations support data collection from several Mattermost servers, which is particularly useful if you’re running Mattermost [in high availability mode](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment). Once you’re tracking system health, you can [set up performance alerts](/administration-guide/scale/performance-alerting) on your Grafana dashboard. Visit the [Mattermost Performance Monitoring product documentation](/administration-guide/scale/deploy-prometheus-grafana-for-performance-monitoring) for installation details.
diff --git a/docs/main/administration-guide/scale/backing-storage-benchmarks.mdx b/docs/main/deployment-guide/reference-architecture/scale/backing-storage-benchmarks.mdx
similarity index 96%
rename from docs/main/administration-guide/scale/backing-storage-benchmarks.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/backing-storage-benchmarks.mdx
index f896d13a2d6f..df6b429419c1 100644
--- a/docs/main/administration-guide/scale/backing-storage-benchmarks.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/backing-storage-benchmarks.mdx
@@ -139,7 +139,7 @@ This page provides detailed write and read benchmark results for supported stora
- For S3 tests, [Amazon S3 exported upload part size](/administration-guide/configure/environment-configuration-settings#amazon-s3-upload-part-size) was set to the default value (100MB).
- Local EBS storage is the stock gp3 (3000 IOPS) provided by EC2 instances.
-- Both EBS and EFS solutions tested are considered `local` storage options from the application's perspective, where the [file storage system](/administration-guide/configure/environment-configuration-settings#file-storage-system) is set to `local` in both cases. EFS is essentially AWS's managed NFS, which enables it to serve as a potential alternative to S3 by allowing multiple Mattermost nodes in a high-availability (HA) deployment to share a common file system. In such HA scenarios, the standard local file storage (e.g., an EBS volume attached to a single instance) [is not suitable, as it can't be shared across multiple nodes](/administration-guide/scale/high-availability-cluster-based-deployment#file-storage). EFS is a good alternative in this case, but EFS is not a block storage solution like EBS.
+- Both EBS and EFS solutions tested are considered `local` storage options from the application's perspective, where the [file storage system](/administration-guide/configure/environment-configuration-settings#file-storage-system) is set to `local` in both cases. EFS is essentially AWS's managed NFS, which enables it to serve as a potential alternative to S3 by allowing multiple Mattermost nodes in a high-availability (HA) deployment to share a common file system. In such HA scenarios, the standard local file storage (e.g., an EBS volume attached to a single instance) [is not suitable, as it can't be shared across multiple nodes](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment#file-storage). EFS is a good alternative in this case, but EFS is not a block storage solution like EBS.
## Supported storage options
diff --git a/docs/main/administration-guide/scale/common-configure-mattermost-for-enterprise-search.mdx b/docs/main/deployment-guide/reference-architecture/scale/common-configure-mattermost-for-enterprise-search.mdx
similarity index 96%
rename from docs/main/administration-guide/scale/common-configure-mattermost-for-enterprise-search.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/common-configure-mattermost-for-enterprise-search.mdx
index 3271e150bbf1..d9c175a6ebd5 100644
--- a/docs/main/administration-guide/scale/common-configure-mattermost-for-enterprise-search.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/common-configure-mattermost-for-enterprise-search.mdx
@@ -38,7 +38,7 @@ For high post volume deployments, we strongly encourage you to read and properly
-For high post volume deployments, we also strongly recommend *disabling* Database Search once Elasticsearch or AWS OpenSearch is fully configured and running. The Mattermost Server will fall back on Database search if Elasticsearch or OpenSearch are unavailable, which can lead to performance degradation on high post volume deployments. From Mattermost v11.7, the server proactively detects outages through periodic health checks and falls back to database search on the first health check failure, rather than waiting for requests to time out. See the [outage handling FAQ](/administration-guide/scale/enterprise-search#how-does-mattermost-handle-elasticsearch-or-opensearch-outages) for details.
+For high post volume deployments, we also strongly recommend *disabling* Database Search once Elasticsearch or AWS OpenSearch is fully configured and running. The Mattermost Server will fall back on Database search if Elasticsearch or OpenSearch are unavailable, which can lead to performance degradation on high post volume deployments. From Mattermost v11.7, the server proactively detects outages through periodic health checks and falls back to database search on the first health check failure, rather than waiting for requests to time out. See the [outage handling FAQ](/deployment-guide/reference-architecture/scale/enterprise-search#how-does-mattermost-handle-elasticsearch-or-opensearch-outages) for details.
diff --git a/docs/main/administration-guide/scale/elasticsearch-setup.mdx b/docs/main/deployment-guide/reference-architecture/scale/elasticsearch-setup.mdx
similarity index 99%
rename from docs/main/administration-guide/scale/elasticsearch-setup.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/elasticsearch-setup.mdx
index daca250abe26..cdc12d945214 100644
--- a/docs/main/administration-guide/scale/elasticsearch-setup.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/elasticsearch-setup.mdx
@@ -107,4 +107,3 @@ Follow these steps to configure Mattermost to use your Elasticsearch server and
3. Ensure **Backend type** is set to `elasticsearch`.
-:start-after: :nosearch:
diff --git a/docs/main/administration-guide/scale/enterprise-search.mdx b/docs/main/deployment-guide/reference-architecture/scale/enterprise-search.mdx
similarity index 94%
rename from docs/main/administration-guide/scale/enterprise-search.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/enterprise-search.mdx
index dfad99c71758..068259bda96f 100644
--- a/docs/main/administration-guide/scale/enterprise-search.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/enterprise-search.mdx
@@ -16,13 +16,13 @@ Mattermost database search starts to show performance degradation at around 2 mi
Elasticsearch is a well-established and widely used search engine with a large ecosystem and community support that provides enterprise-scale deployments with optimized search performance, dedicated indexing, and usage resourcing via cluster support for fast, predicable search results.
-Mattermost's implementation uses [Elasticsearch](https://www.elastic.co) as a distributed, RESTful search engine supporting highly efficient database searches in a [cluster environment](/administration-guide/scale/high-availability-cluster-based-deployment). Learn more about [setting up and configuring Mattermost for an Elasticsearch server](/administration-guide/scale/elasticsearch-setup).
+Mattermost's implementation uses [Elasticsearch](https://www.elastic.co) as a distributed, RESTful search engine supporting highly efficient database searches in a [cluster environment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment). Learn more about [setting up and configuring Mattermost for an Elasticsearch server](/deployment-guide/reference-architecture/scale/elasticsearch-setup).
## AWS OpenSearch Service
AWS OpenSearch Service is the official path forward from Elasticsearch v7.10.x for AWS customers. It's a fully managed service that makes it easy to deploy, operate, and scale OpenSearch clusters in the AWS Cloud to provide a simple and cost-effective way to search, analyze, and visualize data in real time.
-The AWS OpenSearch Service is built on the open-source OpenSearch project, which is a community-driven fork of Elasticsearch. Learn more about [setting up and configuring Mattermost for an OpenSearch server](/administration-guide/scale/opensearch-setup).
+The AWS OpenSearch Service is built on the open-source OpenSearch project, which is a community-driven fork of Elasticsearch. Learn more about [setting up and configuring Mattermost for an OpenSearch server](/deployment-guide/reference-architecture/scale/opensearch-setup).
## Supported paths
@@ -31,12 +31,12 @@ Review the following support paths for enterprise search based on the version yo
-[Elasticsearch v9](https://www.elastic.co/guide/en/elasticsearch/reference/current/elasticsearch-intro.html) is supported from Mattermost v11.7. We recommend upgrading to Elasticsearch v9 for the latest features and security updates. See the [Elasticsearch upgrade](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html) documentation for upgrade details, and see the [Elasticsearch setup](/administration-guide/scale/elasticsearch-setup) documentation for details on configuring your Mattermost deployment to use Elasticsearch.
+[Elasticsearch v9](https://www.elastic.co/guide/en/elasticsearch/reference/current/elasticsearch-intro.html) is supported from Mattermost v11.7. We recommend upgrading to Elasticsearch v9 for the latest features and security updates. See the [Elasticsearch upgrade](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html) documentation for upgrade details, and see the [Elasticsearch setup](/deployment-guide/reference-architecture/scale/elasticsearch-setup) documentation for details on configuring your Mattermost deployment to use Elasticsearch.
-[Elasticsearch v8](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/elasticsearch-intro.html) is supported from Mattermost v9.11. We recommend upgrading to Elasticsearch v9 for the latest features and security updates. See the [Elasticsearch upgrade](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html) documentation for upgrade details, and see the [Elasticsearch setup](/administration-guide/scale/elasticsearch-setup) documentation for details on configuring your Mattermost deployment to use Elasticsearch.
+[Elasticsearch v8](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/elasticsearch-intro.html) is supported from Mattermost v9.11. We recommend upgrading to Elasticsearch v9 for the latest features and security updates. See the [Elasticsearch upgrade](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html) documentation for upgrade details, and see the [Elasticsearch setup](/deployment-guide/reference-architecture/scale/elasticsearch-setup) documentation for details on configuring your Mattermost deployment to use Elasticsearch.
@@ -49,7 +49,7 @@ Elasticsearch v7.17 is no longer supported as of Mattermost v11.7. If you're sti
AWS OpenSearch Service is the official path forward from Elasticsearch v7.10.x for AWS customers to provide a simple and cost-effective way to search, analyze, and visual data in real time. It's essentially a continuation of Elasticsearch v7.10.x but maintained as open source by AWS. It provides long-term support, active development, and compatibility with AWS clients, libraries, and managed services.
-See the **AWS Elasticsearch v7.10.x** tab on this page for details on upgrading to AWS OpenSearch, and see the [AWS OpenSearch setup](/administration-guide/scale/opensearch-setup) documentation for details on configuring your Mattermost deployment to use AWS OpenSearch.
+See the **AWS Elasticsearch v7.10.x** tab on this page for details on upgrading to AWS OpenSearch, and see the [AWS OpenSearch setup](/deployment-guide/reference-architecture/scale/opensearch-setup) documentation for details on configuring your Mattermost deployment to use AWS OpenSearch.
diff --git a/docs/main/administration-guide/scale/estimated-storage-per-user-per-month.mdx b/docs/main/deployment-guide/reference-architecture/scale/estimated-storage-per-user-per-month.mdx
similarity index 100%
rename from docs/main/administration-guide/scale/estimated-storage-per-user-per-month.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/estimated-storage-per-user-per-month.mdx
diff --git a/docs/main/administration-guide/scale/high-availability-cluster-based-deployment.mdx b/docs/main/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment.mdx
similarity index 96%
rename from docs/main/administration-guide/scale/high-availability-cluster-based-deployment.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment.mdx
index fdfe5429d37b..2e6f5fc5d64f 100644
--- a/docs/main/administration-guide/scale/high-availability-cluster-based-deployment.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment.mdx
@@ -30,14 +30,14 @@ Moreover, search replicas are also supported to handle search queries.
## Preparation
-- Review [available reference architectures](/administration-guide/scale/scaling-for-enterprise#scaling-for-enterprise) for guidance on scaling Mattermost for the applicable number of users. Reference architecture guidance includes recommendations for the number of Mattermost nodes, database writer and reader nodes, Elasticsearch nodes, and proxy nodes, as well as file storage estimates depending on anticipated usage patterns.
+- Review [available reference architectures](/deployment-guide/reference-architecture/scale/scaling-for-enterprise#scaling-for-enterprise) for guidance on scaling Mattermost for the applicable number of users. Reference architecture guidance includes recommendations for the number of Mattermost nodes, database writer and reader nodes, Elasticsearch nodes, and proxy nodes, as well as file storage estimates depending on anticipated usage patterns.
- Determine whether the file storage configuration for Mattermost will be Amazon S3, an S3-compatible file storage service, or network-attached storage (NAS) mounted on each Mattermost node. If Mattermost nodes are left configured with local file system storage on the host file system on each node rather than a NAS location, high availability will not function correctly and may corrupt your file storage.
- For Kubernetes deployments, review [Deploy Mattermost on Kubernetes](/deployment-guide/server/deploy-kubernetes).
- For non-Kubernetes deployments, install or upgrade Mattermost to the desired version on one server provisioned for Mattermost. Refer to [Deploy Mattermost on Linux](/deployment-guide/server/deploy-linux) for installation details. [Install a license key](/administration-guide/manage/admin/installing-license-key) to apply an Enterprise or Enterprise Advanced license key to the installed node.
- **Recommended:** If using `config.json` for Mattermost configuration, refer to [Store configuration in your database](/administration-guide/configure/configuration-in-your-database) to migrate the Mattermost instance to using the database for configuration. It is also possible to continue using `config.json` files. However, when high availability is enabled, the System Console is set to read-only mode to ensure all the `config.json` files on the Mattermost servers are always identical.
-- Review [Calls self-hosted deployment](/administration-guide/configure/calls-deployment) to develop an appropriately-scaled Calls deployment plan.
-- If you anticipate your Mattermost server reaching more than 2.5 million posts, review [Enterprise search](/administration-guide/scale/enterprise-search) for options to ensure optimum search performance.
-- For Mattermost deployments for more than 100,000 users, review the [Redis](/administration-guide/scale/redis) deployment guide.
+- Review [Calls self-hosted deployment](/deployment-guide/calls/calls-deployment-guide) to develop an appropriately-scaled Calls deployment plan.
+- If you anticipate your Mattermost server reaching more than 2.5 million posts, review [Enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search) for options to ensure optimum search performance.
+- For Mattermost deployments for more than 100,000 users, review the [Redis](/deployment-guide/reference-architecture/scale/redis) deployment guide.
## Deployment guide
@@ -966,7 +966,7 @@ rm /tmp/search-replica-config.json
Search replicas use the same PostgreSQL configuration as regular read replicas. When configured, all search queries are distributed among the search replicas. If search replicas are unavailable, queries fall back to read replicas, and ultimately to the primary database.
-For deployments requiring advanced search capabilities, see [Enterprise search](/administration-guide/scale/enterprise-search) for information on Elasticsearch integration.
+For deployments requiring advanced search capabilities, see [Enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search) for information on Elasticsearch integration.
#### Database sizing
@@ -1042,15 +1042,15 @@ Once your high availability cluster is deployed and operational, consider these
**Calls deployment**
-If you're using Mattermost Calls for voice and screen sharing communication, review [Calls self-hosted deployment](/administration-guide/configure/calls-deployment) to plan an appropriately-scaled Calls infrastructure that matches your HA deployment.
+If you're using Mattermost Calls for voice and screen sharing communication, review [Calls self-hosted deployment](/deployment-guide/calls/calls-deployment-guide) to plan an appropriately-scaled Calls infrastructure that matches your HA deployment.
**Enterprise search**
-For deployments expected to exceed 2.5 million posts, consider implementing [Enterprise search](/administration-guide/scale/enterprise-search) with Elasticsearch. Elasticsearch provides significantly faster search performance and advanced search capabilities for large-scale deployments.
+For deployments expected to exceed 2.5 million posts, consider implementing [Enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search) with Elasticsearch. Elasticsearch provides significantly faster search performance and advanced search capabilities for large-scale deployments.
**Redis integration**
-For deployments serving more than 100,000 users, implement [Redis](/administration-guide/scale/redis) to improve session management, caching, and real-time communication performance across your cluster.
+For deployments serving more than 100,000 users, implement [Redis](/deployment-guide/reference-architecture/scale/redis) to improve session management, caching, and real-time communication performance across your cluster.
**Performance monitoring**
@@ -1140,7 +1140,7 @@ When you reinstall a plugin in v5.14, the previous **Enabled** or **Disabled** s
### CLI and High Availability
-The CLI is run in a single node which bypasses the mechanisms that a [high availability environment](/administration-guide/scale/high-availability-cluster-based-deployment) uses to perform actions across all nodes in the cluster. As a result, when running [CLI commands](/administration-guide/manage/command-line-tools) in a High Availability environment, tasks such as updating and deleting users or changing configuration settings require a server restart.
+The CLI is run in a single node which bypasses the mechanisms that a [high availability environment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) uses to perform actions across all nodes in the cluster. As a result, when running [CLI commands](/administration-guide/manage/command-line-tools) in a High Availability environment, tasks such as updating and deleting users or changing configuration settings require a server restart.
We recommend using [mmctl](/administration-guide/manage/mmctl-command-line-tool) in a high availability environment instead since a server restart is not required. These changes are made through the API layer, so the node receiving the change request notifies all other nodes in the cluster.
diff --git a/docs/main/administration-guide/scale/lifetime-storage.mdx b/docs/main/deployment-guide/reference-architecture/scale/lifetime-storage.mdx
similarity index 100%
rename from docs/main/administration-guide/scale/lifetime-storage.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/lifetime-storage.mdx
diff --git a/docs/main/administration-guide/scale/opensearch-setup.mdx b/docs/main/deployment-guide/reference-architecture/scale/opensearch-setup.mdx
similarity index 57%
rename from docs/main/administration-guide/scale/opensearch-setup.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/opensearch-setup.mdx
index 5d69f2183a3d..50482c51a6dd 100644
--- a/docs/main/administration-guide/scale/opensearch-setup.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/opensearch-setup.mdx
@@ -30,105 +30,99 @@ Starting in Mattermost v12.0 (October 2026), OpenSearch v1.x is no longer suppor
1. To install on-premise OpenSearch, provision a dedicated server (e.g. Ubuntu 22.04 LTS).
2. Install Java (OpenSearch requires Java 11+):
-> ``` sh
-> sudo apt update
-> sudo apt install -y openjdk-11-jdk
-> java -version
-> ```
+ ``` sh
+ sudo apt update
+ sudo apt install -y openjdk-11-jdk
+ java -version
+ ```
3. Download & extract OpenSearch 2.x:
-> ``` sh
-> wget https://artifacts.opensearch.org/releases/bundle/opensearch/2.9.0/opensearch-2.9.0-linux-x64.tar.gz
-> tar -xzf opensearch-2.9.0-linux-x64.tar.gz
-> sudo mv opensearch-2.9.0 /usr/share/opensearch
-> ```
+ ``` sh
+ wget https://artifacts.opensearch.org/releases/bundle/opensearch/2.9.0/opensearch-2.9.0-linux-x64.tar.gz
+ tar -xzf opensearch-2.9.0-linux-x64.tar.gz
+ sudo mv opensearch-2.9.0 /usr/share/opensearch
+ ```
4. Create a dedicated user & set permissions:
-> ``` sh
-> sudo useradd --no-create-home --shell /bin/false opensearch
-> sudo chown -R opensearch:opensearch /usr/share/opensearch
-> ```
+ ``` sh
+ sudo useradd --no-create-home --shell /bin/false opensearch
+ sudo chown -R opensearch:opensearch /usr/share/opensearch
+ ```
5. Configure systemd:
-> ``` sh
-> [Unit]
-> Description=OpenSearch
-> Wants=network-online.target
-> After=network-online.target
->
-> [Service]
-> Type=notify
-> User=opensearch
-> Group=opensearch
-> ExecStart=/usr/share/opensearch/bin/opensearch
-> Restart=on-failure
-> LimitNOFILE=65536
-> LimitNPROC=4096
->
-> [Install]
-> WantedBy=multi-user.target
-> ```
+ ``` sh
+ [Unit]
+ Description=OpenSearch
+ Wants=network-online.target
+ After=network-online.target
+
+ [Service]
+ Type=notify
+ User=opensearch
+ Group=opensearch
+ ExecStart=/usr/share/opensearch/bin/opensearch
+ Restart=on-failure
+ LimitNOFILE=65536
+ LimitNPROC=4096
+
+ [Install]
+ WantedBy=multi-user.target
+ ```
6. Edit `opensearch.yml` to include the following:
-> ``` yaml
-> cluster.name: mattermost-cluster
-> node.name: node-1
-> path.data: /var/lib/opensearch
-> path.logs: /var/log/opensearch
-> network.host: 0.0.0.0
-> discovery.seed_hosts: ["<other-node-ip>"]
-> cluster.initial_master_nodes: ["node-1", "node-2"]
-> ```
->
->
->
->
->
-> Note
->
->
->
-> Ensure `path.data` and `path.logs` directories exist and are owned by the `opensearch` user before starting the service:
->
-> ``` sh
-> sudo mkdir -p /var/lib/opensearch /var/log/opensearch
-> sudo chown -R opensearch:opensearch /var/lib/opensearch /var/log/opensearch
-> ```
->
->
+ ``` yaml
+ cluster.name: mattermost-cluster
+ node.name: node-1
+ path.data: /var/lib/opensearch
+ path.logs: /var/log/opensearch
+ network.host: 0.0.0.0
+ discovery.seed_hosts: [""]
+ cluster.initial_master_nodes: ["node-1", "node-2"]
+ ```
+
+
+
+ Ensure `path.data` and `path.logs` directories exist and are owned by the `opensearch` user before starting the service:
+
+ ``` sh
+ sudo mkdir -p /var/lib/opensearch /var/log/opensearch
+ sudo chown -R opensearch:opensearch /var/lib/opensearch /var/log/opensearch
+ ```
+
+
7. Enable & start OpenSearch:
-> ``` sh
-> sudo systemctl daemon-reload
-> sudo systemctl enable opensearch
-> sudo systemctl start opensearch
-> sudo systemctl status opensearch
-> ```
+ ``` sh
+ sudo systemctl daemon-reload
+ sudo systemctl enable opensearch
+ sudo systemctl start opensearch
+ sudo systemctl status opensearch
+ ```
8. Install the [icu-analyzer plugin](https://docs.opensearch.org/latest/install-and-configure/additional-plugins/index/) to the `/usr/share/opensearch/plugins` directory by running the following command:
-> ``` sh
-> sudo /usr/share/opensearch/bin/opensearch-plugin install analysis-icu
-> ```
+ ``` sh
+ sudo /usr/share/opensearch/bin/opensearch-plugin install analysis-icu
+ ```
**(Optional) CJK language analyzer plugins**: To improve search for Korean, Japanese, or Chinese content, install one or more of the following language-specific analyzer plugins: `analysis-nori` (Korean), `analysis-kuromoji` (Japanese), and `analysis-smartcn` (Chinese).
-> ``` sh
-> sudo /usr/share/opensearch/bin/opensearch-plugin install analysis-nori
-> sudo /usr/share/opensearch/bin/opensearch-plugin install analysis-kuromoji
-> sudo /usr/share/opensearch/bin/opensearch-plugin install analysis-smartcn
-> ```
+``` sh
+sudo /usr/share/opensearch/bin/opensearch-plugin install analysis-nori
+sudo /usr/share/opensearch/bin/opensearch-plugin install analysis-kuromoji
+sudo /usr/share/opensearch/bin/opensearch-plugin install analysis-smartcn
+```
After installing the CJK plugins, restart OpenSearch to load them:
-> ``` sh
-> sudo systemctl restart opensearch
-> ```
+``` sh
+sudo systemctl restart opensearch
+```
Then enable the [EnableCJKAnalyzers](/administration-guide/configure/environment-configuration-settings#enable-cjk-analyzers) configuration setting. See [Enabling Chinese, Japanese, and Korean Search](/administration-guide/configure/enabling-chinese-japanese-korean-search) for additional CJK search configuration options.
@@ -186,15 +180,15 @@ resource "null_resource" "install_icu_plugin" {
1. To install AWS OpenSearch, open the **AWS Console \> OpenSearch Service**.
2. Create a domain, where:
-> - Domain name: `mattermost-os`
-> - Engine version: `OpenSearch 2.x`.
+ - Domain name: `mattermost-os`
+ - Engine version: `OpenSearch 2.x`.
3. Configure the cluster, where:
-> - instance type: `r6g.xlarge.search`
-> - data nodes: 2
-> - master nodes: 2
-> - storage: EBS gp3 (1536 GiB, 4608 IOPS, 250 MiB/s)
+ - instance type: `r6g.xlarge.search`
+ - data nodes: 2
+ - master nodes: 2
+ - storage: EBS gp3 (1536 GiB, 4608 IOPS, 250 MiB/s)
4. Specify the network for: VPC with 2 subnets, and a security group allowing Mattermost IPs on port `443`.
@@ -206,16 +200,16 @@ Port 9200 is commonly used for local or on-premise OpenSearch. The AWS OpenSearc
5. Configure the access policy (JSON). Mattermost doesn't sign OpenSearch requests with AWS SigV4, so restrict access at the network layer with the VPC and security group from step 4, and use an open principal in the domain access policy:
-> ``` sh
-> {
-> "Version": "2012-10-17",
-> "Statement": [{
-> "Effect": "Allow",
-> "Principal": { "AWS": "*" },
-> "Action": "es:ESHttp*",
-> "Resource": "arn:aws:es:us-east-1:123456789012:domain/mattermost-os/*" }]
-> }
-> ```
+ ``` sh
+ {
+ "Version": "2012-10-17",
+ "Statement": [{
+ "Effect": "Allow",
+ "Principal": { "AWS": "*" },
+ "Action": "es:ESHttp*",
+ "Resource": "arn:aws:es:us-east-1:123456789012:domain/mattermost-os/*" }]
+ }
+ ```
@@ -225,96 +219,96 @@ IAM principal-based access policies (for example, `"Principal": { "AWS": "arn:aw
6. Configure the following advanced settings (JSON):
-> ``` sh
-> {
-> "action.destructive_requires_name": "false",
-> "rest.action.multi.allow_explicit_index": "true",
-> "indices.query.bool.max_clause_count": "1024",
-> "indices.fielddata.cache.size": "20"
-> }
-> ```
+ ``` sh
+ {
+ "action.destructive_requires_name": "false",
+ "rest.action.multi.allow_explicit_index": "true",
+ "indices.query.bool.max_clause_count": "1024",
+ "indices.fielddata.cache.size": "20"
+ }
+ ```
7. Configure the automated snapshot start hour as 23 (UTC), enforce HTTPS, then review & create.
8. To test, run the following command:
-> ``` sh
-> curl https://mattermost-os-xxxxxxxxxxx.us-east-1.es.amazonaws.com
-> ```
+ ``` sh
+ curl https://mattermost-os-xxxxxxxxxxx.us-east-1.es.amazonaws.com
+ ```
## AWS Terraform Example
-> ``` sh
-> provider "aws" {
-> region = "us-east-1"
-> }
->
-> resource "aws_iam_role" "os_service_role" {
-> name = "OSServiceRole"
-> assume_role_policy = < {
-> "Version": "2012-10-17",
-> "Statement": [{
-> "Action": "sts:AssumeRole",
-> "Effect": "Allow",
-> "Principal": { "Service": "es.amazonaws.com" }
-> }]
-> }
-> EOF
-> }
->
-> resource "aws_opensearch_domain" "mattermost" {
-> domain_name = "mattermost-os"
-> engine_version = "OpenSearch_2.9"
-> cluster_config {
-> instance_type = "r6g.xlarge.search"
-> instance_count = 2
-> dedicated_master_enabled = true
-> dedicated_master_type = "r6g.xlarge.search"
-> dedicated_master_count = 2
-> zone_awareness_enabled = true
-> }
->
-> ebs_options {
-> ebs_enabled = true
-> volume_type = "gp3"
-> volume_size = 1536
-> iops = 4608
-> }
->
-> vpc_options {
-> subnet_ids = ["subnet-blah1", "subnet-blah2"]
-> security_group_ids = ["sg-1234567890"]
-> }
->
-> advanced_options = {
-> "rest.action.multi.allow_explicit_index" = "true"
-> "indices.query.bool.max_clause_count" = "1024"
-> "indices.fielddata.cache.size" = "20"
-> "action.destructive_requires_name" = "false"
-> }
->
-> access_policies = < {
-> "Version": "2012-10-17",
-> "Statement": [{
-> "Effect": "Allow",
-> "Principal": {
-> "AWS": "*"
-> },
-> "Action": "es:ESHttp*",
-> "Resource": "arn:aws:es:us-east-1:123456789012:domain/mattermost-os/*"
-> }]
-> }
-> POLICY
-> service_software_options {
-> automated_snapshot_start_hour = 23
-> }
->
-> domain_endpoint_options {
-> enforce_https = true
-> }
-> }
-> ```
+``` sh
+provider "aws" {
+ region = "us-east-1"
+}
+
+resource "aws_iam_role" "os_service_role" {
+ name = "OSServiceRole"
+ assume_role_policy = <
diff --git a/docs/main/administration-guide/scale/redis.mdx b/docs/main/deployment-guide/reference-architecture/scale/redis.mdx
similarity index 100%
rename from docs/main/administration-guide/scale/redis.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/redis.mdx
diff --git a/docs/main/administration-guide/scale/scale-to-100000-users.mdx b/docs/main/deployment-guide/reference-architecture/scale/scale-to-100000-users.mdx
similarity index 97%
rename from docs/main/administration-guide/scale/scale-to-100000-users.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/scale-to-100000-users.mdx
index 60be3fa40680..38ae86fc2732 100644
--- a/docs/main/administration-guide/scale/scale-to-100000-users.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/scale-to-100000-users.mdx
@@ -7,7 +7,7 @@ import Inc2_additional_ha_considerations from './additional-ha-considerations.md
-This page describes the Mattermost reference architecture designed for the load of up to 100000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/administration-guide/scale/scaling-for-enterprise) documentation for details.
+This page describes the Mattermost reference architecture designed for the load of up to 100000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) documentation for details.
- **High Availability**: Required
- **Database Configuration**: writer, multiple readers
diff --git a/docs/main/administration-guide/scale/scale-to-15000-users.mdx b/docs/main/deployment-guide/reference-architecture/scale/scale-to-15000-users.mdx
similarity index 95%
rename from docs/main/administration-guide/scale/scale-to-15000-users.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/scale-to-15000-users.mdx
index 51e851b61b16..558e6d23e5b7 100644
--- a/docs/main/administration-guide/scale/scale-to-15000-users.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/scale-to-15000-users.mdx
@@ -7,7 +7,7 @@ import Inc2_additional_ha_considerations from './additional-ha-considerations.md
-This page describes the Mattermost reference architecture designed for the load of up to 15000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/administration-guide/scale/scaling-for-enterprise) documentation for details.
+This page describes the Mattermost reference architecture designed for the load of up to 15000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) documentation for details.
- **High Availability**: Required
- **Database Configuration**: writer, reader
diff --git a/docs/main/administration-guide/scale/scale-to-200-users.mdx b/docs/main/deployment-guide/reference-architecture/scale/scale-to-200-users.mdx
similarity index 95%
rename from docs/main/administration-guide/scale/scale-to-200-users.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/scale-to-200-users.mdx
index 900a4ef74598..0f152aa2e01c 100644
--- a/docs/main/administration-guide/scale/scale-to-200-users.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/scale-to-200-users.mdx
@@ -4,7 +4,7 @@ title: "Scale Mattermost up to 200 users"
import Inc0_lifetime_storage from './lifetime-storage.mdx';
import Inc1_estimated_storage_per_user_per_month from './estimated-storage-per-user-per-month.mdx';
-This page describes the Mattermost reference architecture designed for the load of up to 200 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/administration-guide/scale/scaling-for-enterprise) documentation for details.
+This page describes the Mattermost reference architecture designed for the load of up to 200 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) documentation for details.
- **High Availability**: Not required
- **Database Configuration**: Single
diff --git a/docs/main/administration-guide/scale/scale-to-2000-users.mdx b/docs/main/deployment-guide/reference-architecture/scale/scale-to-2000-users.mdx
similarity index 96%
rename from docs/main/administration-guide/scale/scale-to-2000-users.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/scale-to-2000-users.mdx
index cc8b2c487829..b1ab85067580 100644
--- a/docs/main/administration-guide/scale/scale-to-2000-users.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/scale-to-2000-users.mdx
@@ -7,7 +7,7 @@ import Inc2_additional_ha_considerations from './additional-ha-considerations.md
-This page describes the Mattermost reference architecture designed for a minimum load of 100 concurrent users and up to 2000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/administration-guide/scale/scaling-for-enterprise) documentation for details.
+This page describes the Mattermost reference architecture designed for a minimum load of 100 concurrent users and up to 2000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) documentation for details.
- **High Availability**: Required
- **Database Configuration**: writer, reader
diff --git a/docs/main/administration-guide/scale/scale-to-200000-users.mdx b/docs/main/deployment-guide/reference-architecture/scale/scale-to-200000-users.mdx
similarity index 98%
rename from docs/main/administration-guide/scale/scale-to-200000-users.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/scale-to-200000-users.mdx
index 02bb164e995c..6fff47aaf4c7 100644
--- a/docs/main/administration-guide/scale/scale-to-200000-users.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/scale-to-200000-users.mdx
@@ -7,7 +7,7 @@ import Inc2_additional_ha_considerations from './additional-ha-considerations.md
-This page describes the Mattermost reference architecture designed for the load of up to 200000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/administration-guide/scale/scaling-for-enterprise) documentation for details.
+This page describes the Mattermost reference architecture designed for the load of up to 200000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) documentation for details.
- **High Availability**: Required
- **Database Configuration**: writer, multiple readers
diff --git a/docs/main/administration-guide/scale/scale-to-30000-users.mdx b/docs/main/deployment-guide/reference-architecture/scale/scale-to-30000-users.mdx
similarity index 95%
rename from docs/main/administration-guide/scale/scale-to-30000-users.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/scale-to-30000-users.mdx
index 20684e7ea87f..5c1ead2ae4c4 100644
--- a/docs/main/administration-guide/scale/scale-to-30000-users.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/scale-to-30000-users.mdx
@@ -7,7 +7,7 @@ import Inc2_additional_ha_considerations from './additional-ha-considerations.md
-This page describes the Mattermost reference architecture designed for the load of up to 30000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/administration-guide/scale/scaling-for-enterprise) documentation for details.
+This page describes the Mattermost reference architecture designed for the load of up to 30000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) documentation for details.
- **High Availability**: Required
- **Database Configuration**: writer, multiple readers
diff --git a/docs/main/administration-guide/scale/scale-to-50000-users.mdx b/docs/main/deployment-guide/reference-architecture/scale/scale-to-50000-users.mdx
similarity index 95%
rename from docs/main/administration-guide/scale/scale-to-50000-users.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/scale-to-50000-users.mdx
index eacccd17a6d6..c97e5ee6200d 100644
--- a/docs/main/administration-guide/scale/scale-to-50000-users.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/scale-to-50000-users.mdx
@@ -7,7 +7,7 @@ import Inc2_additional_ha_considerations from './additional-ha-considerations.md
-This page describes the Mattermost reference architecture designed for the load of up to 50000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/administration-guide/scale/scaling-for-enterprise) documentation for details.
+This page describes the Mattermost reference architecture designed for the load of up to 50000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) documentation for details.
- **High Availability**: Required
- **Database Configuration**: writer, multiple readers
diff --git a/docs/main/administration-guide/scale/scale-to-80000-users.mdx b/docs/main/deployment-guide/reference-architecture/scale/scale-to-80000-users.mdx
similarity index 95%
rename from docs/main/administration-guide/scale/scale-to-80000-users.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/scale-to-80000-users.mdx
index 844b8d70ec8d..465a7d8f0ee2 100644
--- a/docs/main/administration-guide/scale/scale-to-80000-users.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/scale-to-80000-users.mdx
@@ -7,7 +7,7 @@ import Inc2_additional_ha_considerations from './additional-ha-considerations.md
-This page describes the Mattermost reference architecture designed for the load of up to 80000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/administration-guide/scale/scaling-for-enterprise) documentation for details.
+This page describes the Mattermost reference architecture designed for the load of up to 80000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) documentation for details.
- **High Availability**: Required
- **Database Configuration**: writer, multiple readers
diff --git a/docs/main/administration-guide/scale/scale-to-90000-users.mdx b/docs/main/deployment-guide/reference-architecture/scale/scale-to-90000-users.mdx
similarity index 95%
rename from docs/main/administration-guide/scale/scale-to-90000-users.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/scale-to-90000-users.mdx
index 38373d74bca0..66b5134cf5ec 100644
--- a/docs/main/administration-guide/scale/scale-to-90000-users.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/scale-to-90000-users.mdx
@@ -7,7 +7,7 @@ import Inc2_additional_ha_considerations from './additional-ha-considerations.md
-This page describes the Mattermost reference architecture designed for the load of up to 90000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/administration-guide/scale/scaling-for-enterprise) documentation for details.
+This page describes the Mattermost reference architecture designed for the load of up to 90000 concurrent users. Unsure which reference architecture to use? See the [scaling for enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) documentation for details.
- **High Availability**: Required
- **Database Configuration**: writer, multiple readers
diff --git a/docs/main/administration-guide/scale/scaling-for-enterprise.mdx b/docs/main/deployment-guide/reference-architecture/scale/scaling-for-enterprise.mdx
similarity index 67%
rename from docs/main/administration-guide/scale/scaling-for-enterprise.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/scaling-for-enterprise.mdx
index b1a7f8826161..2d5391ca30c3 100644
--- a/docs/main/administration-guide/scale/scaling-for-enterprise.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/scaling-for-enterprise.mdx
@@ -13,35 +13,35 @@ Server requirements vary based on usage and we highly recommend that you run a p
## Backing storage
-Review detailed [write and read storage benchmark results](/administration-guide/scale/backing-storage-benchmarks) for supported storage options including local file system (EBS, gp3), network file system (EFS), and object storage (S3) to make informed decisions based on your use case and infrastructure needs.
+Review detailed [write and read storage benchmark results](/deployment-guide/reference-architecture/scale/backing-storage-benchmarks) for supported storage options including local file system (EBS, gp3), network file system (EFS), and object storage (S3) to make informed decisions based on your use case and infrastructure needs.
## Enterprise search
We highly recommend a dedicated server for large enterprise deployments to run highly efficient database searches in a cluster environment.
-For deployments with over 5 million posts, [Enterprise search](/administration-guide/scale/enterprise-search) using [Elasticsearch](/administration-guide/scale/enterprise-search#elasticsearch) or [AWS OpenSearch Service](/administration-guide/scale/enterprise-search#aws-opensearch-service) is required for optimized search performance, dedicated indexing and usage resourcing via cluster support without performance degradation and timeouts, resulting in faster, more predicable search results.
+For deployments with over 5 million posts, [Enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search) using [Elasticsearch](/deployment-guide/reference-architecture/scale/enterprise-search#elasticsearch) or [AWS OpenSearch Service](/deployment-guide/reference-architecture/scale/enterprise-search#aws-opensearch-service) is required for optimized search performance, dedicated indexing and usage resourcing via cluster support without performance degradation and timeouts, resulting in faster, more predicable search results.
## High availability
-A [high availability cluster-based deployment](/administration-guide/scale/high-availability-cluster-based-deployment) enables a Mattermost system to maintain service during outages and hardware failures through the use of redundant infrastructure.
+A [high availability cluster-based deployment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) enables a Mattermost system to maintain service during outages and hardware failures through the use of redundant infrastructure.
## Redis
-[Redis](/administration-guide/scale/redis) is an in-memory data structure store that can be used as a database, cache, and message broker. Mattermost uses Redis as an external cache to improve performance at scale. When properly configured, Redis can help support Mattermost installations with more than 100,000 users by providing improved performance through efficient caching.
+[Redis](/deployment-guide/reference-architecture/scale/redis) is an in-memory data structure store that can be used as a database, cache, and message broker. Mattermost uses Redis as an external cache to improve performance at scale. When properly configured, Redis can help support Mattermost installations with more than 100,000 users by providing improved performance through efficient caching.
## Available reference architectures
The following reference architectures are available as recommended starting points for your self-hosted Mattermost deployment, where user counts refer to the number of concurrent users for a given deployment. The number of concurrent numbers is commonly lower than the total number of user accounts.
-- [Scale up to 200 users](/administration-guide/scale/scale-to-200-users) - Learn how to scale Mattermost to up to 200 users.
-- [Scale up to 2000 users](/administration-guide/scale/scale-to-2000-users) - Learn how to scale Mattermost to up to 2000 users.
-- [Scale up to 15000 users](/administration-guide/scale/scale-to-15000-users) - Learn how to scale Mattermost to up to 15000 users.
-- [Scale up to 30000 users](/administration-guide/scale/scale-to-30000-users) - Learn how to scale Mattermost to up to 30000 users.
-- [Scale up to 50000 users](/administration-guide/scale/scale-to-50000-users) - Learn how to scale Mattermost to up to 50000 users.
-- [Scale up to 80000 users](/administration-guide/scale/scale-to-80000-users) - Learn how to scale Mattermost to up to 80000 users.
-- [Scale up to 90000 users](/administration-guide/scale/scale-to-90000-users) - Learn how to scale Mattermost to up to 90000 users.
-- [Scale up to 100000 users](/administration-guide/scale/scale-to-100000-users) - Learn how to scale Mattermost to up to 100000 users.
-- [Scale up to 200000 users](/administration-guide/scale/scale-to-200000-users) - Learn how to scale Mattermost to up to 200000 users.
+- [Scale up to 200 users](/deployment-guide/reference-architecture/scale/scale-to-200-users) - Learn how to scale Mattermost to up to 200 users.
+- [Scale up to 2000 users](/deployment-guide/reference-architecture/scale/scale-to-2000-users) - Learn how to scale Mattermost to up to 2000 users.
+- [Scale up to 15000 users](/deployment-guide/reference-architecture/scale/scale-to-15000-users) - Learn how to scale Mattermost to up to 15000 users.
+- [Scale up to 30000 users](/deployment-guide/reference-architecture/scale/scale-to-30000-users) - Learn how to scale Mattermost to up to 30000 users.
+- [Scale up to 50000 users](/deployment-guide/reference-architecture/scale/scale-to-50000-users) - Learn how to scale Mattermost to up to 50000 users.
+- [Scale up to 80000 users](/deployment-guide/reference-architecture/scale/scale-to-80000-users) - Learn how to scale Mattermost to up to 80000 users.
+- [Scale up to 90000 users](/deployment-guide/reference-architecture/scale/scale-to-90000-users) - Learn how to scale Mattermost to up to 90000 users.
+- [Scale up to 100000 users](/deployment-guide/reference-architecture/scale/scale-to-100000-users) - Learn how to scale Mattermost to up to 100000 users.
+- [Scale up to 200000 users](/deployment-guide/reference-architecture/scale/scale-to-200000-users) - Learn how to scale Mattermost to up to 200000 users.
@@ -57,7 +57,7 @@ At a high level, each deployment size was fixed (Mattermost server node count/si
Tests were defined by configuration of the actions executed by each simulated user (and the frequency of these actions) where the coordinator metrics define a health system under load. Tests were performed using the Mattermost v9.5 Extended Support Release (ESR). Job servers weren't used. All tests with more than a single app node had an NGINX proxy running in front of them.
-For the last test of 200K users, further infrastructure changes were made. Elasticsearch nodes were added. A Redis instance was added, and multiple NGINX proxies were used to distribute traffic evenly across all nodes in the cluster. More details can be found on the [scale to 200000 users](/administration-guide/scale/scale-to-200000-users) documentation page.
+For the last test of 200K users, further infrastructure changes were made. Elasticsearch nodes were added. A Redis instance was added, and multiple NGINX proxies were used to distribute traffic evenly across all nodes in the cluster. More details can be found on the [scale to 200000 users](/deployment-guide/reference-architecture/scale/scale-to-200000-users) documentation page.
Full testing methodology, configuration, and setup is available, incluidng a [fixed database dump with 100 million posts](https://us-east-1.console.aws.amazon.com/backup/home?region=us-east-1#/resources/arn%3Aaws%3Ards%3Aus-east-1%3A729462591288%3Acluster%3Adb-pg-100m-posts-v9-5-5). Visit the [Mattermost Community](https://community.mattermost.com/) and join the [Developers: Performance channel](https://community.mattermost.com/core/channels/developers-performance) for details.
diff --git a/docs/main/administration-guide/scale/server-architecture.mdx b/docs/main/deployment-guide/reference-architecture/scale/server-architecture.mdx
similarity index 88%
rename from docs/main/administration-guide/scale/server-architecture.mdx
rename to docs/main/deployment-guide/reference-architecture/scale/server-architecture.mdx
index 7c9cbebaf7dd..dd378a722c3e 100644
--- a/docs/main/administration-guide/scale/server-architecture.mdx
+++ b/docs/main/deployment-guide/reference-architecture/scale/server-architecture.mdx
@@ -5,7 +5,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
-The following diagrams detail suggested architecture configurations of [high availability Mattermost deployments](/administration-guide/scale/high-availability-cluster-based-deployment#deployment-guide) at different scales. Hardware and infrastructure requirements will vary significantly based on usage and policies. See the [scaling for enterprise](/administration-guide/scale/scaling-for-enterprise) documentation for reference architecture guidance at scale, including hardware and infrastructure requirements.
+The following diagrams detail suggested architecture configurations of [high availability Mattermost deployments](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment#deployment-guide) at different scales. Hardware and infrastructure requirements will vary significantly based on usage and policies. See the [scaling for enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) documentation for reference architecture guidance at scale, including hardware and infrastructure requirements.
High availability in Mattermost consists of running redundant Mattermost application servers, redundant database servers, and redundant load balancers so that failure of any one of these components does not interrupt operation of the system. Upon failure of one component, the remaining application servers, database servers, and load balancers must be sized and configured to carry the full load of the system. If this requirement is not met, an outage of one component can result in an overload of the remaining components, causing a complete system outage.
@@ -19,7 +19,7 @@ You can apply most configuration changes and dot release security updates withou
## Designed for scale
-Mattermost is designed to be able to handle a large number of concurrent users, and the architecture can be scaled up or down as needed. The architecture is also designed to be flexible, allowing for the addition of new components or services as needed. The following diagrams show the recommended architecture for Mattermost deployments at 5,000, 10,000, 25,000, and 50,000 users. The diagrams are organized by user count and include a general diagram, AWS and Azure versions of each diagram, and an Oracle diagram for 5,000 users. See the [scaling for enterprise](/administration-guide/scale/scaling-for-enterprise) documentation for more information on scaling Mattermost deployments.
+Mattermost is designed to be able to handle a large number of concurrent users, and the architecture can be scaled up or down as needed. The architecture is also designed to be flexible, allowing for the addition of new components or services as needed. The following diagrams show the recommended architecture for Mattermost deployments at 5,000, 10,000, 25,000, and 50,000 users. The diagrams are organized by user count and include a general diagram, AWS and Azure versions of each diagram, and an Oracle diagram for 5,000 users. See the [scaling for enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) documentation for more information on scaling Mattermost deployments.
- Each generalized diagram represents a full High Availability deployment across all critical components. The proxy, database, file storage, and Elasticsearch layers can be replaced by cloud services.
- Each AWS diagram represents a full High Availability deployment on Amazon Web Services making full use of the available services.
diff --git a/docs/main/deployment-guide/server/deploy-containers.mdx b/docs/main/deployment-guide/server/deploy-containers.mdx
index ef78a2a2493d..6d21eeb7882b 100644
--- a/docs/main/deployment-guide/server/deploy-containers.mdx
+++ b/docs/main/deployment-guide/server/deploy-containers.mdx
@@ -201,7 +201,7 @@ Looking for a way to evaluate Mattermost on a single local machine using Docker?
- This local image is self-contained (i.e., it has an internal database and works out of the box). Dropping a container using this image removes data and configuration as expected. You can see the [configuration settings](/administration-guide/configure/configuration-settings) documentation to learn more about customizing your trial deployment.
- **Preview Mode** shouldn't be used in a production environment, as it uses a known password string, contains other non-production configuration settings, has email disabled, keeps no persistent data (all data lives inside the container), and doesn't support upgrades.
-- If you are planning to use the calling functionality in **Preview Mode** on a non-local environment, you should ensure that the server is running on a secure (HTTPs) connection and that the [network requirements](/administration-guide/configure/calls-rtcd-setup#network-requirements) to run calls are met.
+- If you are planning to use the calling functionality in **Preview Mode** on a non-local environment, you should ensure that the server is running on a secure (HTTPs) connection and that the [network requirements](/deployment-guide/calls/calls-rtcd-setup#network-requirements) to run calls are met.
diff --git a/docs/main/deployment-guide/server/kubernetes/deploy-k8s.mdx b/docs/main/deployment-guide/server/kubernetes/deploy-k8s.mdx
index 585a25bf6c6e..1550aa3388c3 100644
--- a/docs/main/deployment-guide/server/kubernetes/deploy-k8s.mdx
+++ b/docs/main/deployment-guide/server/kubernetes/deploy-k8s.mdx
@@ -15,7 +15,7 @@ Before you begin, ensure you have the following:
- A functioning Kubernetes cluster (see the [Kubernetes setup guide](https://kubernetes.io/docs/setup/)). Your cluster should be running a [supported Kubernetes version](https://kubernetes.io/releases/).
- The kubectl command-line tool installed on your local machine (see the [kubectl installation guide](https://kubernetes.io/docs/reference/kubectl/)).
- A fundamental understanding of Kubernetes concepts, such as deployments, pods, and applying manifests.
-- Sufficient Kubernetes resources allocated based on your expected user load. Consult the [scaling for Enterprise](/administration-guide/scale/scaling-for-enterprise#available-reference-architectures) documentation for resource requirements at different scales.
+- Sufficient Kubernetes resources allocated based on your expected user load. Consult the [scaling for Enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise#available-reference-architectures) documentation for resource requirements at different scales.
## Installation steps
@@ -66,7 +66,7 @@ The Mattermost Kubernetes Operator can be installed using Helm.
- A Mattermost Enterprise license is required for multi-server deployments.
-- For single-server deployments without an Enterprise license, add `Replicas: 1` to the `spec` section in step 2 below. See the [high availability documentation](/administration-guide/scale/high-availability-cluster-based-deployment) for more on highly-available deployments.
+- For single-server deployments without an Enterprise license, add `Replicas: 1` to the `spec` section in step 2 below. See the [high availability documentation](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) for more on highly-available deployments.
diff --git a/docs/main/deployment-guide/server/linux/deploy-azure-native-vm.mdx b/docs/main/deployment-guide/server/linux/deploy-azure-native-vm.mdx
index 5220ddbf481a..5fff2e5a7977 100644
--- a/docs/main/deployment-guide/server/linux/deploy-azure-native-vm.mdx
+++ b/docs/main/deployment-guide/server/linux/deploy-azure-native-vm.mdx
@@ -46,7 +46,7 @@ On the **Basics** tab, configure the following:
On the **Application** tab, configure how the Mattermost application is sized and how administrators access the VMs.
-1. **Scaling tier:** Pick the user scale you are planning for. The wizard uses this to suggest sensible defaults for the **VMSS instance count**, **VM size**, and **PostgreSQL SKU**, and to determine whether **read replicas** are created for the database. The dropdown shows the recommended node count and VM SKU for each tier. For the underlying sizing rationale, see [available reference architectures](/administration-guide/scale/scaling-for-enterprise#available-reference-architectures).
+1. **Scaling tier:** Pick the user scale you are planning for. The wizard uses this to suggest sensible defaults for the **VMSS instance count**, **VM size**, and **PostgreSQL SKU**, and to determine whether **read replicas** are created for the database. The dropdown shows the recommended node count and VM SKU for each tier. For the underlying sizing rationale, see [available reference architectures](/deployment-guide/reference-architecture/scale/scaling-for-enterprise#available-reference-architectures).
2. **VMSS instance count:** Number of Mattermost application nodes (1–5). Defaults to the suggested value for your scaling tier; adjust if needed.
3. **Enterprise license file:** Required when the instance count is greater than 1 (multi-node high-availability clustering). Upload your Mattermost Enterprise license file. The license is optional for single-node deployments.
4. **VM SKU:** Size of each VMSS instance. The wizard surfaces recommended Linux sizes first; you can select any supported Linux size available in your region.
@@ -111,7 +111,7 @@ Congratulations! You've successfully deployed a production-ready Mattermost stac
## Next steps
-- For sizing guidance and reference architectures, see [high-availability-cluster-based-deployment](/administration-guide/scale/high-availability-cluster-based-deployment) and [server architecture](/administration-guide/scale/server-architecture).
+- For sizing guidance and reference architectures, see [high-availability-cluster-based-deployment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) and [server architecture](/deployment-guide/reference-architecture/scale/server-architecture).
- For ongoing operations (upgrades, backups, monitoring), follow the standard Mattermost server administration documentation.
## Runbooks
diff --git a/docs/main/deployment-guide/server/linux/deploy-rhel.mdx b/docs/main/deployment-guide/server/linux/deploy-rhel.mdx
index 277fb74a4f76..324a7b9d5647 100644
--- a/docs/main/deployment-guide/server/linux/deploy-rhel.mdx
+++ b/docs/main/deployment-guide/server/linux/deploy-rhel.mdx
@@ -250,7 +250,7 @@ sudo firewall-cmd --reload
- Port 8045 (TCP) is the rtcd API.
- TCP support for RTC requires Calls v0.17+ and rtcd v0.11+.
-See the [Calls deployment guide](/administration-guide/configure/calls-deployment-guide) for the full topology.
+See the [Calls deployment guide](/deployment-guide/calls/calls-deployment-guide) for the full topology.
@@ -314,7 +314,7 @@ allow perm=execute exe=/opt/rtcd/bin/rtcd : dir=/opt/rtcd all trust=0
allow perm=execute exe=/usr/lib/systemd/systemd trust=1 : dir=/opt/rtcd/ all trust=0
```
-Then reload fapolicyd as above. See [RTCD setup and configuration](/administration-guide/configure/calls-deployment-guide) for the full rtcd install path.
+Then reload fapolicyd as above. See [RTCD setup and configuration](/deployment-guide/calls/calls-rtcd-setup) for the full rtcd install path.
diff --git a/docs/main/deployment-guide/software-hardware-requirements.mdx b/docs/main/deployment-guide/software-hardware-requirements.mdx
index 13cd7bcfb185..a40942444be1 100644
--- a/docs/main/deployment-guide/software-hardware-requirements.mdx
+++ b/docs/main/deployment-guide/software-hardware-requirements.mdx
@@ -248,7 +248,7 @@ Search limitations on PostgreSQL:
- Terms containing a dash return incorrect results as dashes are ignored in the search query.
- Limitations set by [PostgreSQL itself](https://www.postgresql.org/docs/current/textsearch-limitations.html):
- One of them is: `The length of a tsvector (lexemes + positions) must be less than 1 megabyte`, which means that, based on the file content, even files with content less than 1 MB won't be searchable if they hit the `tsvector` limit of 1 MB.
-- If any of the above is an issue, you can [set up and enable enterprise search](/administration-guide/scale/enterprise-search).
+- If any of the above is an issue, you can [set up and enable enterprise search](/deployment-guide/reference-architecture/scale/enterprise-search).
##### MySQL Support
@@ -305,7 +305,7 @@ Most small to medium Mattermost team deployments can be supported on a single se
#### Scale requirements
-For Enterprise Edition deployments with a multi-server setup, see [our scaling guide](/administration-guide/scale/scaling-for-enterprise).
+For Enterprise Edition deployments with a multi-server setup, see [our scaling guide](/deployment-guide/reference-architecture/scale/scaling-for-enterprise).
It is highly recommended that pilots are run before enterprise-wide deployments in order to estimate full scale usage based on your specific organizational needs. You can use the Mattermost open source load testing framework to simulate usage of your system: [https://github.com/mattermost/mattermost-load-test-ng](https://github.com/mattermost/mattermost-load-test-ng).
@@ -317,5 +317,5 @@ For Enterprise Edition deployments with a multi-server setup, we highly recommen
- Prometheus to track system health of your Mattermost deployment, through [performance monitoring feature](/administration-guide/scale/deploy-prometheus-grafana-for-performance-monitoring) available in Mattermost Enterprise.
- Grafana to visualize the system health metrics collected by Prometheus with the [performance monitoring feature](/administration-guide/scale/deploy-prometheus-grafana-for-performance-monitoring). Grafana 5.0.0 and later is recommended.
-- Elasticsearch to support highly efficient database searches in a cluster environment. Elasticsearch v8.x and v9.x are supported, and Elasticsearch v9.x or AWS OpenSearch is recommended. [Learn more](/administration-guide/scale/enterprise-search).
+- Elasticsearch to support highly efficient database searches in a cluster environment. Elasticsearch v8.x and v9.x are supported, and Elasticsearch v9.x or AWS OpenSearch is recommended. [Learn more](/deployment-guide/reference-architecture/scale/enterprise-search).
- AWS S3 or any S3-compatible service. Mattermost is compatible with object storage systems which implement the S3 API. You can also use local storage or a network drive using NFS. Learn more about file storage configuration options [in our documentation](/administration-guide/configure/environment-configuration-settings#file-storage).
diff --git a/docs/main/end-user-guide/collaborate/audio-and-screensharing.mdx b/docs/main/end-user-guide/collaborate/audio-and-screensharing.mdx
index 5063510b6534..bff8bce013ed 100644
--- a/docs/main/end-user-guide/collaborate/audio-and-screensharing.mdx
+++ b/docs/main/end-user-guide/collaborate/audio-and-screensharing.mdx
@@ -3,7 +3,7 @@ title: "Audio and Screensharing"
---
-Mattermost Calls offers native real-time chat, self-hosted audio calls, and screen sharing within your own network, enabling secure, effective team communication and collaboration. Learn more about [deploying Mattermost Calls](/administration-guide/configure/calls-deployment-guide) in a self-hosted environment and [making calls](/end-user-guide/collaborate/make-calls) with Mattermost.
+Mattermost Calls offers native real-time chat, self-hosted audio calls, and screen sharing within your own network, enabling secure, effective team communication and collaboration. Learn more about [deploying Mattermost Calls](/deployment-guide/calls/calls-deployment-guide) in a self-hosted environment and [making calls](/end-user-guide/collaborate/make-calls) with Mattermost.
With calls and screen sharing, Mattermost ensures that communications remain uninterrupted, even during maintenance or outages, and scales effortlessly to meet your team’s growing needs, safeguarding the integrity of mission-critical operations.
diff --git a/docs/main/end-user-guide/collaborate/make-calls.mdx b/docs/main/end-user-guide/collaborate/make-calls.mdx
index aa43db402fdd..b3548b8b01f6 100644
--- a/docs/main/end-user-guide/collaborate/make-calls.mdx
+++ b/docs/main/end-user-guide/collaborate/make-calls.mdx
@@ -13,7 +13,7 @@ Using a web browser, the desktop app, or the mobile app, you can [join a call](#
- All Mattermost customers can start, join, and participate in 1:1 audio calls with optional screen sharing.
- For group calls up to 50 concurrent users, Mattermost Enterprise, Professional, or Mattermost Cloud is required.
-- Enterprise customers can also [record calls](#record-a-call), enable [live text captions](/end-user-guide/collaborate/make-calls#live-captions-during-calls) during calls, and [transcribe recorded calls](#transcribe-recorded-calls). We recommend that Enterprise self-hosted customers looking for group calls beyond 50 concurrent users consider using the [dedicated RTCD service](/administration-guide/configure/calls-deployment-guide).
+- Enterprise customers can also [record calls](#record-a-call), enable [live text captions](/end-user-guide/collaborate/make-calls#live-captions-during-calls) during calls, and [transcribe recorded calls](#transcribe-recorded-calls). We recommend that Enterprise self-hosted customers looking for group calls beyond 50 concurrent users consider using the [dedicated RTCD service](/deployment-guide/calls/calls-deployment-guide).
- Mattermost Cloud users can start calling right out of the box. For Mattermost self-hosted deployments, System admins need to enable and configure the plugin [using the System Console](/administration-guide/configure/plugins-configuration-settings#calls).
diff --git a/docs/main/for/sre.mdx b/docs/main/for/sre.mdx
index 0d83203c465e..8d0ea1344f83 100644
--- a/docs/main/for/sre.mdx
+++ b/docs/main/for/sre.mdx
@@ -22,10 +22,10 @@ You own Mattermost's deployment, scaling, and operations. This page is a curated
## Common tasks
diff --git a/docs/main/index.mdx b/docs/main/index.mdx
index 002951521f41..c358d6f8d295 100644
--- a/docs/main/index.mdx
+++ b/docs/main/index.mdx
@@ -76,7 +76,7 @@ See the [Client availability](/end-user-guide/access/client-availability) docume
- Enables [1:1 and group audio calls](/end-user-guide/collaborate/make-calls#join-a-call) directly within channels and direct messages, maintaining contextual awareness and access control based on channel membership.
- Supports secure [screen sharing](/end-user-guide/collaborate/make-calls#share-your-screen) for visual coordination and analysis.
-- Operates in [sovereign, air-gapped, or sensitive network](/administration-guide/configure/calls-deployment-guide) environments.
+- Operates in [sovereign, air-gapped, or sensitive network](/deployment-guide/calls/calls-deployment-guide) environments.
- Offers optional [AI-based transcription](/end-user-guide/collaborate/make-calls#transcribe-recorded-calls) and [summarization](/end-user-guide/agents#analyze-threads-and-channels) for meeting capture and follow ups.
- Works across web, desktop, and mobile for flexible, secure access.
@@ -153,8 +153,8 @@ IME runs on lightweight, ruggedized, or mobile infrastructure, optimized for dis
For high-security environments requiring full infrastructure control, IME supports scalable, highly available deployment within sovereign datacenters.
- [Kubernetes-native architecture](/deployment-guide/server/deploy-kubernetes) enables containerized services, self-healing workloads, and zero-downtime updates.
-- [High availability](/administration-guide/scale/high-availability-cluster-based-deployment) through clustering across application, database, and proxy layers.
-- [Horizontal scalability](/administration-guide/scale/scaling-for-enterprise) to tens of thousands of users per instance.
+- [High availability](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) through clustering across application, database, and proxy layers.
+- [Horizontal scalability](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) to tens of thousands of users per instance.
- Complies with Security Technical Implementation Guide (STIG), Federal Information Processing Standard 140-3 (FIPS 140-3), and Federal Risk and Authorization Management Program (FedRAMP)-aligned security standards.
### Hyperscaler & sovereign Cloud support
diff --git a/docs/main/product-overview/certifications-and-compliance.mdx b/docs/main/product-overview/certifications-and-compliance.mdx
index 3ff565c2b6d7..4d075f11cc02 100644
--- a/docs/main/product-overview/certifications-and-compliance.mdx
+++ b/docs/main/product-overview/certifications-and-compliance.mdx
@@ -202,7 +202,7 @@ However, pursuant to Section (b) of Article 49 of GDPR, transfers of personal da
Yes, the Mattermost platform is compliant with IPv6 when Audio & Screen Sharing is disabled, both for our [self-hosted and Cloud offerings](/product-overview/editions-and-offerings).
-We plan to add IPv6 compliance for [Audio & Screen Sharing](/administration-guide/configure/calls-deployment-guide) in future.
+We plan to add IPv6 compliance for [Audio & Screen Sharing](/deployment-guide/calls/calls-deployment-guide) in future.
### Are you 508 compliant?
diff --git a/docs/main/product-overview/editions-and-offerings.mdx b/docs/main/product-overview/editions-and-offerings.mdx
index de673b177d0a..578567d6794d 100644
--- a/docs/main/product-overview/editions-and-offerings.mdx
+++ b/docs/main/product-overview/editions-and-offerings.mdx
@@ -47,7 +47,7 @@ Built for **multi-domain secure operations**, Enterprise Advanced builds on all
Mattermost Enterprise supports large-scale, mission-critical **secure collaborative workflows** with robust security, compliance, and productivity tooling. It builds on core ChatOps capabilities from the Professional offering, plus:
-- [Enterprise-scale search with dedicated indexing and usage resourcing via cluster support](/administration-guide/scale/enterprise-search).
+- [Enterprise-scale search with dedicated indexing and usage resourcing via cluster support](/deployment-guide/reference-architecture/scale/enterprise-search).
- [Sychronization of access controls, channels, and teams with AD/LDAP Groups](/administration-guide/onboard/ad-ldap-groups-synchronization).
- [eDiscovery and compliance export automation](/administration-guide/comply/compliance-export).
- [Enterprise mobile device management with custom EMM support via AppConfig](/deployment-guide/mobile/deploy-mobile-apps-using-emm-provider).
@@ -60,8 +60,8 @@ Mattermost Enterprise supports large-scale, mission-critical **secure collaborat
- [Channel export](/administration-guide/comply/export-mattermost-channel-data)
- [Enhanced compliance controls and granular audit logs with data export](/administration-guide/manage/logging#audit-logging).
- [Advanced collaboration with connected workspaces across Mattermost instances](/administration-guide/onboard/connected-workspaces).
-- [High availability support with multi-node database deployment](/administration-guide/scale/high-availability-cluster-based-deployment).
-- [Horizontal scaling through cluster-based deployment](/administration-guide/scale/scaling-for-enterprise).
+- [High availability support with multi-node database deployment](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment).
+- [Horizontal scaling through cluster-based deployment](/deployment-guide/reference-architecture/scale/scaling-for-enterprise).
- [Advanced performance monitoring](/administration-guide/scale/deploy-prometheus-grafana-for-performance-monitoring).
- [Server health checks](/administration-guide/manage/request-server-health-check).
- [Eligibility for Premier Support add-on](https://mattermost.com/support/).
diff --git a/docs/main/product-overview/faq-enterprise.mdx b/docs/main/product-overview/faq-enterprise.mdx
index dc3b819fafb5..6a1cc69d7c54 100644
--- a/docs/main/product-overview/faq-enterprise.mdx
+++ b/docs/main/product-overview/faq-enterprise.mdx
@@ -66,7 +66,7 @@ Growing your Mattermost installation from supporting a team to supporting an ent
**Functional Scaling:** Scaling from a team to an enterprise is like going from a "virtual office" to a "virtual campus". Advanced features like enterprise authentication, granular permissions, compliance and auditing, and advanced reporting become increasingly important as organizations grow beyond teams. Organizations needing this flexibility can easily upgrade from Mattermost Team Edition to Mattermost Enterprise Edition as well as downgrade without data loss, should their needs change.
-For more information on how Mattermost scales, technically, and functionally, talk to a [Mattermost Expert](https://mattermost.com/contact-sales/), and [read about scaling for Enterprise](/administration-guide/scale/scaling-for-enterprise).
+For more information on how Mattermost scales, technically, and functionally, talk to a [Mattermost Expert](https://mattermost.com/contact-sales/), and [read about scaling for Enterprise](/deployment-guide/reference-architecture/scale/scaling-for-enterprise).
## What does it take to manage a Mattermost deployment?
diff --git a/docs/main/product-overview/plans.mdx b/docs/main/product-overview/plans.mdx
index 2187770a948f..f84641c5bdfc 100644
--- a/docs/main/product-overview/plans.mdx
+++ b/docs/main/product-overview/plans.mdx
@@ -202,7 +202,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
| Multi-team collaboration |
| Work across teams and organizations with real-time calling and screen share, guest accounts to integrate internal and external stakeholders, customer user groups to organize teams within teams, and system-wide notifications to share organization-wide messages. |
- | Group calling and screen share: Streamline real-time collaboration with complete privacy by enabling group audio calling and screenshare up to approximately 50 concurrent users in any group call per self-hosted server. High-scale options for private, self-hosted group calling and screen share are available in Mattermost Enterprise with the setup of its horizontal scaling option. |
+ Group calling and screen share: Streamline real-time collaboration with complete privacy by enabling group audio calling and screenshare up to approximately 50 concurrent users in any group call per self-hosted server. High-scale options for private, self-hosted group calling and screen share are available in Mattermost Enterprise with the setup of its horizontal scaling option. |
| * | }) | }) | }) | v9.11+ |
@@ -241,7 +241,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
| }) | | }) | }) | v9.11+ |
- | High availability, horizontally scalable calls and screen share: Enable high-scale, high availability deployment of audio calling and screen share through dedicated servers managed on an integrated Kubernetes platform. |
+ High availability, horizontally scalable calls and screen share: Enable high-scale, high availability deployment of audio calling and screen share through dedicated servers managed on an integrated Kubernetes platform. |
| | | }) | }) | v9.11+ |
diff --git a/docs/main/use-case-guide/devops-collaboration.mdx b/docs/main/use-case-guide/devops-collaboration.mdx
index e8c7ab931e94..f38b3ceb9239 100644
--- a/docs/main/use-case-guide/devops-collaboration.mdx
+++ b/docs/main/use-case-guide/devops-collaboration.mdx
@@ -39,7 +39,7 @@ Real-time visibility and structured collaboration are critical during service de
- **Automate incident handling** with [Playbooks](/end-user-guide/workflow-automation) to track diagnostics, assign tasks, and issue updates—supporting NOC, SRE, and AppSec workflows.
- **Accelerate containment and recovery** by [integrating alerting tools](/integrations-guide/integrations-guide-index#webhooks) like PagerDuty, Opsgenie, and custom webhooks into secure Mattermost channels.
-- **Ensure communication continuity** during outages using [high availability architecture](/administration-guide/scale/high-availability-cluster-based-deployment) and [support for disconnected environments](/deployment-guide/reference-architecture/deployment-scenarios/air-gapped-deployment).
+- **Ensure communication continuity** during outages using [high availability architecture](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) and [support for disconnected environments](/deployment-guide/reference-architecture/deployment-scenarios/air-gapped-deployment).
- **Enable forensic review and audit** with [logging and export capabilities](/administration-guide/manage/logging#audit-logging) that preserve all incident-related communications.
## Policy-Driven Collaboration in Regulated Environments
diff --git a/docs/main/use-case-guide/integrated-security-operations.mdx b/docs/main/use-case-guide/integrated-security-operations.mdx
index 7d1172d7ab2e..ab813327b051 100644
--- a/docs/main/use-case-guide/integrated-security-operations.mdx
+++ b/docs/main/use-case-guide/integrated-security-operations.mdx
@@ -45,7 +45,7 @@ Cross-organizational threat intelligence teams,spanning sectors, regions, and pu
- **Support multinational and sectoral collaboration** with [custom terms of service enforcement](/administration-guide/comply/custom-terms-of-service) and [localized UI settings](/end-user-guide/preferences/manage-your-display-options#language) for global partner access.
- **Preserve operational trust and compliance** through [role-based access controls](/administration-guide/onboard/advanced-permissions) and [channel-specific permissions](/administration-guide/manage/team-channel-members#advanced-access-controls) that enforce jurisdictional and information-sharing agreements.
- **Operationalize shared threat intelligence** by integrating IOCs, threat actor profiles, and shared playbooks into your Mattermost instance via the [integrations platform](/integrations-guide/integrations-guide-index).
-- **Scale communication globally** with Mattermost's [high availability and horizontal scalability architecture](/administration-guide/scale/scaling-for-enterprise),supporting tens of thousands of users across enterprise, field, government, or classified environments.
+- **Scale communication globally** with Mattermost's [high availability and horizontal scalability architecture](/deployment-guide/reference-architecture/scale/scaling-for-enterprise),supporting tens of thousands of users across enterprise, field, government, or classified environments.
## Get Started
diff --git a/docs/main/use-case-guide/on-prem-skype-for-business-replacement.mdx b/docs/main/use-case-guide/on-prem-skype-for-business-replacement.mdx
index 7ecf369da262..bef480d09879 100644
--- a/docs/main/use-case-guide/on-prem-skype-for-business-replacement.mdx
+++ b/docs/main/use-case-guide/on-prem-skype-for-business-replacement.mdx
@@ -17,7 +17,7 @@ Organizations operating in fully disconnected or classified environments require
- **Ensure secure communication in fully disconnected networks** using Mattermost's support for private on-premise deployments, including FIPS 140-3 validated and DISA STIG-hardened container images. [Learn more](/deployment-guide/reference-architecture/application-architecture) about Mattermost's architecture, components, and backend infrastructure.
- **Maintain operational continuity** with enterprise-grade [channel-based collaboration](/end-user-guide/messaging-collaboration)— including [1:1 audio calls](/end-user-guide/collaborate/make-calls), [screen sharing](/end-user-guide/collaborate/make-calls#share-your-screen), [threaded messaging](/end-user-guide/collaborate/organize-conversations), and [file sharing](/end-user-guide/collaborate/share-files-in-messages)—entirely within air-gapped systems.
-- **Scale to mission requirements** with a [high-availability, horizontally scalable architecture](/administration-guide/scale/scaling-for-enterprise) that supports tens of thousands of users in secure on-prem environments.
+- **Scale to mission requirements** with a [high-availability, horizontally scalable architecture](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) that supports tens of thousands of users in secure on-prem environments.
- **Preserve data sovereignty and eliminate external dependencies** with a self-hosted [Kubernetes deployment model](/deployment-guide/server/deploy-kubernetes) that integrates into classified networks, sovereign data centers, or **Azure Local** (formerly Azure Stack HCI) for hybrid cloud on-premises scenarios.
## Modernize Secure Collaboration Workflows
diff --git a/docs/main/use-case-guide/out-of-band-incident-response.mdx b/docs/main/use-case-guide/out-of-band-incident-response.mdx
index 45bf747b2a80..310f6bc5cb94 100644
--- a/docs/main/use-case-guide/out-of-band-incident-response.mdx
+++ b/docs/main/use-case-guide/out-of-band-incident-response.mdx
@@ -29,7 +29,7 @@ Outages and downtime threaten both productivity and revenue. In large enterprise
**Benefits**
-- **Scale communication globally** with Mattermost's [high availability and horizontal scalability architecture](/administration-guide/scale/scaling-for-enterprise), supporting tens of thousands of users across enterprise, field, government, or classified environments.
+- **Scale communication globally** with Mattermost's [high availability and horizontal scalability architecture](/deployment-guide/reference-architecture/scale/scaling-for-enterprise), supporting tens of thousands of users across enterprise, field, government, or classified environments.
- **Accelerate outage recovery** using [Collaborative Playbooks](/end-user-guide/workflow-automation) that automate response steps and ensure team accountability during time-critical events, reducing mean time to recovery (MTTR) by up to 50%.
- **Demonstrate ROI through measurable outcomes** with built-in metrics tracking incident response times, team coordination efficiency, and compliance audit trails.
diff --git a/docs/main/use-case-guide/purpose-built-collaboration.mdx b/docs/main/use-case-guide/purpose-built-collaboration.mdx
index 646721003504..54db19a828ed 100644
--- a/docs/main/use-case-guide/purpose-built-collaboration.mdx
+++ b/docs/main/use-case-guide/purpose-built-collaboration.mdx
@@ -17,7 +17,7 @@ Coordinating logistics across continents, agencies, and time zones requires a se
- **Enable real-time coordination** across supply chains, procurement, and field units with [channel-based messaging](/end-user-guide/messaging-collaboration) and [playbook-driven workflows](/end-user-guide/workflow-automation) that standardize communication and reduce friction.
- **Connect systems across logistics networks** by integrating ERP, fleet tracking, maintenance management, and transportation tools via [webhooks, APIs, and plugins](/integrations-guide/integrations-guide-index).
-- **Preserve operational continuity** during outages or disruptions using [self-hosted deployments](/deployment-guide/server/server-deployment-planning#deployment-options) and [high availability architecture](/administration-guide/scale/high-availability-cluster-based-deployment) that eliminate reliance on third-party cloud services.
+- **Preserve operational continuity** during outages or disruptions using [self-hosted deployments](/deployment-guide/server/server-deployment-planning#deployment-options) and [high availability architecture](/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment) that eliminate reliance on third-party cloud services.
- **Support multilingual coordination** with [localized UI options](/end-user-guide/preferences/manage-your-display-options#language) in 20+ languages to ensure inclusive collaboration across global teams.
## Operational Technology and ICS Collaboration
diff --git a/docs/main/use-case-guide/secure-command-and-control.mdx b/docs/main/use-case-guide/secure-command-and-control.mdx
index ec3980a5a90f..44a58eb05ea4 100644
--- a/docs/main/use-case-guide/secure-command-and-control.mdx
+++ b/docs/main/use-case-guide/secure-command-and-control.mdx
@@ -32,7 +32,7 @@ Disconnected environments demand resilient tools that work without cloud access,
- **Operate in air-gapped and disconnected networks** using [self-hosted Kubernetes deployments](/deployment-guide/server/deploy-kubernetes) and STIG-hardened container images for secure offline operations.
- **Ensure secure mobile access on managed or BYOD devices** with [mobile security features](/deployment-guide/mobile/mobile-security-features), Zero Trust enforcement, and [ID-only push notifications](/administration-guide/configure/push-notification-server-configuration-settings#id-only-push-notifications) for sensitive alerts.
- **Integrate with legacy and mission-specific systems** to maintain decision advantage in disconnected environments through [custom-built, self-hosted integrations](/integrations-guide/integrations-guide-index) tailored to your operational infrastructure.
-- **Maintain command resilience** using [high availability cluster-based deployment](/administration-guide/scale/scaling-for-enterprise) and [horizontal scalability](/administration-guide/scale/scaling-for-enterprise) to support operational continuity at scale.
+- **Maintain command resilience** using [high availability cluster-based deployment](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) and [horizontal scalability](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) to support operational continuity at scale.
- **Automate field workflows** with [Collaborative Playbooks](/end-user-guide/workflow-automation) that track tasks, manage field updates, and orchestrate responses under DDIL constraints.
- **Enable secure real-time collaboration with headquarters** using [Connected Workspaces](/administration-guide/onboard/connected-workspaces) to synchronize discussions, files, and reactions if connectivity is restored.
diff --git a/docs/main/use-case-guide/self-sovereign-collaboration.mdx b/docs/main/use-case-guide/self-sovereign-collaboration.mdx
index 3ac578a9079e..74fd113add07 100644
--- a/docs/main/use-case-guide/self-sovereign-collaboration.mdx
+++ b/docs/main/use-case-guide/self-sovereign-collaboration.mdx
@@ -28,7 +28,7 @@ From national critical infrastructure to defense-grade networks, Mattermost offe
- **Deploy in classified, air-gapped, or disconnected environments** using [Kubernetes-based deployments](/deployment-guide/server/deploy-kubernetes) and STIG-hardened container images to support classified operations and sensitive data workflows.
- **Eliminate third-party monitoring** with full control over infrastructure, encryption keys, access policies, and system-level logging.
-- **Scale to meet operational growth** with [horizontal scalability architecture](/administration-guide/scale/scaling-for-enterprise) that supports tens of thousands of users in sovereign environments without degrading performance or control.
+- **Scale to meet operational growth** with [horizontal scalability architecture](/deployment-guide/reference-architecture/scale/scaling-for-enterprise) that supports tens of thousands of users in sovereign environments without degrading performance or control.
- **Maintain operational continuity under cyber or supply chain disruption** using fully self-managed infrastructure that ensures collaboration continues even during cloud outages or external service failures.
## Interoperable Mission-Partner Collaboration
diff --git a/docs/pdf/books/administration-guide.json b/docs/pdf/books/administration-guide.json
index 2ef6070c0560..fca8369d5c53 100644
--- a/docs/pdf/books/administration-guide.json
+++ b/docs/pdf/books/administration-guide.json
@@ -16,10 +16,6 @@
"/administration-guide/comply/legal-hold",
"/administration-guide/configure/agents-admin-guide",
"/administration-guide/configure/bleve-search",
- "/administration-guide/configure/calls-deployment-guide",
- "/administration-guide/configure/calls-kubernetes",
- "/administration-guide/configure/calls-logging",
- "/administration-guide/configure/calls-offloader-setup",
"/administration-guide/configure/cloud-billing-account-settings",
"/administration-guide/configure/configuration-in-your-database",
"/administration-guide/configure/configuration-settings",
diff --git a/docs/pdf/books/deployment-guide.json b/docs/pdf/books/deployment-guide.json
index 7a048e822cd4..efde2732d1f0 100644
--- a/docs/pdf/books/deployment-guide.json
+++ b/docs/pdf/books/deployment-guide.json
@@ -31,6 +31,12 @@
"/deployment-guide/server/pre-authentication-secrets",
"/deployment-guide/server/image-proxy",
"/deployment-guide/server/prepare-mattermost-mysql-database",
+ "/deployment-guide/calls/calls-deployment-guide",
+ "/deployment-guide/calls/calls-kubernetes",
+ "/deployment-guide/calls/calls-logging",
+ "/deployment-guide/calls/calls-metrics-monitoring",
+ "/deployment-guide/calls/calls-offloader-setup",
+ "/deployment-guide/calls/calls-rtcd-setup",
"/deployment-guide/desktop/desktop-app-deployment",
"/deployment-guide/desktop/desktop-app-managed-resources",
"/deployment-guide/desktop/desktop-custom-dictionaries",
diff --git a/docs/site/README.md b/docs/site/README.md
index 700bf05594bf..340d9a4eb84d 100644
--- a/docs/site/README.md
+++ b/docs/site/README.md
@@ -57,19 +57,45 @@ top of the auto-generated tree for those sections only: Overview
(`DEPLOYMENT_GROUPS`/`DEPLOYMENT_ROOT_ORDER`), Administration Guide →
Configure (`ADMIN_CONFIGURE_GROUPS`/`ADMIN_CONFIGURE_ORDER`),
Administration Guide → Manage (`ADMIN_MANAGE_GROUPS`/`ADMIN_MANAGE_ORDER`),
-and Integrations Guide (`INTEGRATIONS_GROUPS`/`INTEGRATIONS_ROOT_ORDER`).
-
-The override only changes how the sidebar renders — files stay flat on
-disk at their existing paths, so URLs don't move. Each override is a pair
-of constants near the top of the script:
+Administration Guide → Onboard (`ADMIN_ONBOARD_GROUPS`/`ADMIN_ONBOARD_ORDER`),
+Administration Guide → Scale (`ADMIN_SCALE_GROUPS`/`ADMIN_SCALE_ORDER`),
+End User Guide → Collaborate (`COLLABORATE_GROUPS`/`COLLABORATE_ORDER`), and
+Integrations Guide (`INTEGRATIONS_GROUPS`/`INTEGRATIONS_ROOT_ORDER`).
+
+Usually the override only changes how the sidebar renders — files stay
+flat on disk at their existing paths, so URLs don't move. The one
+exception is Administration Guide → Scale: 21 of its 28 files (capacity
+planning, HA/architecture, search infrastructure, caching) were physically
+moved to `deployment-guide/reference-architecture/scale/` and are grouped
+via `DEPLOYMENT_GROUPS.scaling`, matching where Sphinx's live nav actually
+lists that content (under Deployment Guide → Reference Architecture, as a
+sibling of Application Architecture). Only the 7 monitoring/observability
+pages stayed under `ADMIN_SCALE_GROUPS`. Since that move changes URLs, the
+Sphinx→Docusaurus redirect targets for those 21 pages in
+`sidebars/active-redirects.json` were updated to point at the new paths,
+and internal `.mdx` links across the docs were rewritten accordingly.
+
+Each override is a pair of constants near the top of the script:
- A `*_GROUPS` map of group key → `{label, landing?, items}`, where `items`
are doc basenames (relative to that section's directory) or nested
- inline group objects.
+ inline group objects. A group's `items` can itself contain a nested
+ `{label, items}` sub-group, which adds a third category level — a
+ four-level path of Guide → Group → Sub-group → page — for a section
+ large enough to need it — see `OVERVIEW_GROUPS.subscription`'s "Cloud"
+ sub-group, or `ADMIN_MANAGE_GROUPS.userAccess`'s "Attribute-Based Access
+ Control" sub-group, for existing examples.
- A `*_ROOT_ORDER`/`*_ORDER` array listing the top-level order: plain
strings for standalone docs, `{group: 'key'}` for a group from the map
above.
+There's no single shared "grouping engine" — each overridden section gets
+its own small `buildXItem`/`regroupX` (or `buildXSidebar`) pair that mirrors
+the shape of the others (see `buildCollaborateItem`/`regroupCollaborate` for
+the End User Guide → Collaborate one). Adding an override for a new section
+means copying that shape for the new section, not extending a shared
+function.
+
**Adding a new file to one of these sections:** the script fails loudly if
you forget it — it logs a `WARN: N file(s) missing from *_ORDER` and falls
back to appending the orphaned file(s) at the root of that section, so a
diff --git a/docs/site/scripts/gen-documentation-sidebar.mjs b/docs/site/scripts/gen-documentation-sidebar.mjs
index 424a33547bb5..c0d4a44a8bec 100644
--- a/docs/site/scripts/gen-documentation-sidebar.mjs
+++ b/docs/site/scripts/gen-documentation-sidebar.mjs
@@ -29,17 +29,29 @@ const OUT = join(SITE_ROOT, 'sidebars', 'documentation.generated.json');
// Most sections build their sidebar straight from the filesystem: each
// subdirectory becomes a category, each file a doc, sorted by
// `sidebar_position` frontmatter then filename (see buildCategory below).
-// Overview, Deployment Guide, Administration Guide > Configure, and
-// Integrations Guide are flat piles of 15-40 files that read badly as one
-// long alphabetical list, so each gets a manual grouping override applied
-// at sidebar-render time only — the files themselves stay flat on disk, so
-// URLs don't move.
+// Overview, Deployment Guide, Administration Guide > Configure/Manage/
+// Onboard/Scale, End User Guide > Collaborate, and Integrations Guide are
+// flat piles of 15-49 files that read badly as one long alphabetical list,
+// so each gets a manual grouping override applied at sidebar-render time
+// only — the files themselves stay flat on disk, so URLs don't move.
//
// Each override is a `*_GROUPS` map (group key -> {label, landing?, items})
// plus a `*_ROOT_ORDER`/`*_ORDER` array giving the top-level order (plain
// strings for standalone docs, `{group: 'key'}` for a group from the map).
// A `*_HIDDEN` set lists files that got re-parented into a group so the
-// orphan check below doesn't re-append them at the section root.
+// orphan check below doesn't re-append them at the section root. A group's
+// `items` can itself contain nested `{label, items}` sub-groups (see e.g.
+// OVERVIEW_GROUPS.subscription's "Cloud" sub-group below) — that's what
+// gets you a 3rd level of TOC nesting (Guide > Group > Sub-group > page)
+// when a section's flat list is large enough to need it.
+//
+// This pattern isn't a single generic engine — each section with an
+// override gets its own small `buildXItem`/`regroupX` pair (see
+// buildCollaborateItem/regroupCollaborate for the newest one) that mirrors
+// the others in shape. Adding an override for a new section means copying
+// that shape, not extending a shared function; sections without one of
+// these overrides just render every level of their filesystem tree as-is
+// (buildCategory already recurses to unlimited depth on its own).
//
// Adding a new file to one of these sections: add its basename to the
// relevant group's `items` (or to the root order array, if standalone). If
@@ -216,6 +228,61 @@ const DEPLOYMENT_GROUPS = {
],
},
+ // Scaling Architecture — moved here (physically, from
+ // administration-guide/scale/) to mirror Sphinx's live nav, where this
+ // whole cluster (capacity planning, HA/architecture, search infra, caching)
+ // sits under Reference Architecture as a sibling of Application
+ // Architecture. `scaling-for-enterprise` is the general entry point
+ // referencing the sub-groups below, so it's the group's landing page.
+ scaling: {
+ label: 'Scaling Architecture',
+ landing: 'reference-architecture/scale/scaling-for-enterprise',
+ items: [
+ {label: 'Capacity Planning', items: [
+ 'reference-architecture/scale/scale-to-200-users',
+ 'reference-architecture/scale/scale-to-2000-users',
+ 'reference-architecture/scale/scale-to-15000-users',
+ 'reference-architecture/scale/scale-to-30000-users',
+ 'reference-architecture/scale/scale-to-50000-users',
+ 'reference-architecture/scale/scale-to-80000-users',
+ 'reference-architecture/scale/scale-to-90000-users',
+ 'reference-architecture/scale/scale-to-100000-users',
+ 'reference-architecture/scale/scale-to-200000-users',
+ 'reference-architecture/scale/estimated-storage-per-user-per-month',
+ 'reference-architecture/scale/backing-storage-benchmarks',
+ 'reference-architecture/scale/lifetime-storage',
+ 'reference-architecture/scale/additional-ha-considerations',
+ ]},
+ {label: 'High Availability & Architecture', items: [
+ 'reference-architecture/scale/high-availability-cluster-based-deployment',
+ 'reference-architecture/scale/server-architecture',
+ ]},
+ {label: 'Search Infrastructure', landing: 'reference-architecture/scale/enterprise-search', items: [
+ 'reference-architecture/scale/elasticsearch-setup',
+ 'reference-architecture/scale/opensearch-setup',
+ 'reference-architecture/scale/common-configure-mattermost-for-enterprise-search',
+ ]},
+ {label: 'Caching', items: [
+ 'reference-architecture/scale/redis',
+ ]},
+ ],
+ },
+
+ // Calls Deployment & Configuration — moved here from Administration
+ // Guide → Configure. RTCD, Offloader, Kubernetes, logging, and metrics
+ // are deployment/operations concerns, not settings-reference material.
+ calls: {
+ label: 'Calls Deployment & Configuration',
+ landing: 'calls/calls-deployment-guide',
+ items: [
+ 'calls/calls-rtcd-setup',
+ 'calls/calls-offloader-setup',
+ 'calls/calls-kubernetes',
+ 'calls/calls-logging',
+ 'calls/calls-metrics-monitoring',
+ ],
+ },
+
// Backup & Disaster Recovery — group the two related pages.
backupDr: {
label: 'Backup & Disaster Recovery',
@@ -267,6 +334,8 @@ const DEPLOYMENT_ROOT_ORDER = [
{group: 'deploymentScenarios'},
'deployment-architecture',
{group: 'server'},
+ {group: 'scaling'},
+ {group: 'calls'},
// Desktop, Mobile, Air-Gapped Operations keep their auto-generated trees
// (each has its own index file + sub-pages). Referenced by the `__auto__`
// sentinel so we slot them in here, in the order we want.
@@ -305,8 +374,8 @@ const DEPLOYMENT_HIDDEN = new Set([
//
// Configure is a flat 34-file settings-reference dump. This override groups
// it by task/subsystem so the ~12 "*-configuration-settings" reference pages
-// don't drown the handful of task-oriented pages (Search, Calls, Storage,
-// Email, Billing, Branding) sitting alongside them at the same level.
+// don't drown the handful of task-oriented pages (Search, Email, Billing,
+// Branding) sitting alongside them at the same level.
//
// AI Agents Configuration is deliberately kept as its own standalone,
// un-grouped top-level entry (not folded into a "misc/optional" bucket) —
@@ -338,25 +407,6 @@ const ADMIN_CONFIGURE_GROUPS = {
'enabling-chinese-japanese-korean-search',
],
},
- calls: {
- label: 'Calls Deployment & Configuration',
- landing: 'calls-deployment-guide',
- items: [
- 'calls-rtcd-setup',
- 'calls-offloader-setup',
- 'calls-kubernetes',
- 'calls-logging',
- 'calls-metrics-monitoring',
- ],
- },
- storage: {
- label: 'Storage & Database',
- items: [
- 'configuration-in-your-database',
- 'azure-blob-storage',
- 'environment-variables',
- ],
- },
email: {
label: 'Email & Notifications',
items: [
@@ -406,8 +456,9 @@ const ADMIN_CONFIGURE_ORDER = [
{group: 'settingsReference'},
{group: 'search'},
{group: 'agents'},
- {group: 'calls'},
- {group: 'storage'},
+ 'configuration-in-your-database',
+ 'environment-variables',
+ 'azure-blob-storage',
{group: 'email'},
{group: 'billing'},
{group: 'branding'},
@@ -426,9 +477,6 @@ const ADMIN_CONFIGURE_HIDDEN = new Set([
'rate-limiting-configuration-settings', 'push-notification-server-configuration-settings',
'experimental-configuration-settings', 'deprecated-configuration-settings',
'bleve-search', 'enabling-chinese-japanese-korean-search',
- 'calls-rtcd-setup', 'calls-offloader-setup', 'calls-kubernetes',
- 'calls-logging', 'calls-metrics-monitoring',
- 'configuration-in-your-database', 'azure-blob-storage', 'environment-variables',
'smtp-email', 'email-templates',
'self-hosted-account-settings', 'cloud-billing-account-settings',
'custom-branding-tools', 'customize-mattermost', 'optimize-your-workspace',
@@ -559,6 +607,283 @@ const ADMIN_MANAGE_HIDDEN = new Set([
'bulk-export-tool', 'admin/migration',
]);
+// ---------------------------------------------------------------------------
+// End User Guide — Collaborate — manual grouping override.
+// ---------------------------------------------------------------------------
+//
+// Collaborate is a flat 49-file dump (Channels, Messaging, Calls, Teams, and
+// Accessibility topics all interleaved alphabetically) — the section
+// End-user Guide > Collaborate feedback (Eric Sethna review, item 6) called
+// out as "overwhelming". This override groups it by topic, same pattern as
+// Administration Guide's Configure/Manage/Onboard/Scale (see #37591/#37630).
+//
+// `collaborate-within-channels` doubles as both the Channels group's landing
+// page and a regular grouped item — it already reads as a "Channels" hub
+// page in its own "Learn more" section, which the `channels` group's item
+// list below mirrors.
+
+const COLLABORATE_GROUPS = {
+ channels: {
+ label: 'Channels',
+ landing: 'collaborate-within-channels',
+ items: [
+ 'channel-types',
+ 'browse-channels',
+ 'create-channels',
+ 'join-leave-channels',
+ 'navigate-between-channels',
+ 'channel-naming-conventions',
+ 'channel-header-purpose',
+ 'rename-channels',
+ 'archive-unarchive-channels',
+ 'favorite-channels',
+ 'mark-channels-unread',
+ 'manage-channel-members',
+ 'manage-channel-bookmarks',
+ 'display-channel-banners',
+ 'autotranslate-messages',
+ 'convert-public-channels',
+ 'convert-group-messages',
+ ],
+ },
+ messaging: {
+ label: 'Messaging & Threads',
+ items: [
+ 'send-messages',
+ 'communicate-with-messages',
+ 'reply-to-messages',
+ 'organize-conversations',
+ 'format-messages',
+ 'mark-messages-unread',
+ 'mention-people',
+ 'message-priority',
+ 'message-reminders',
+ 'schedule-messages',
+ 'save-pin-messages',
+ 'flag-messages',
+ 'forward-messages',
+ 'search-for-messages',
+ 'share-links',
+ 'share-files-in-messages',
+ 'react-with-emojis-gifs',
+ ],
+ },
+ calls: {
+ label: 'Calls & Screen Sharing',
+ items: [
+ 'make-calls',
+ 'audio-and-screensharing',
+ ],
+ },
+ teamsAndRoles: {
+ label: 'Teams, Groups & Roles',
+ items: [
+ 'learn-about-roles',
+ 'organize-using-teams',
+ 'team-settings',
+ 'organize-using-custom-user-groups',
+ ],
+ },
+ integrations: {
+ label: 'Integrations & Connected Apps',
+ items: [
+ 'extend-mattermost-with-integrations',
+ 'agents-context-management',
+ 'collaborate-within-connected-microsoft-teams',
+ ],
+ },
+ accessibility: {
+ label: 'Keyboard Shortcuts & Accessibility',
+ items: [
+ 'keyboard-shortcuts',
+ 'team-keyboard-shortcuts',
+ 'keyboard-accessibility',
+ 'view-system-information',
+ ],
+ },
+};
+
+// Top-level Collaborate order. Strings are doc basenames relative to
+// end-user-guide/collaborate/; objects reference COLLABORATE_GROUPS keys.
+const COLLABORATE_ORDER = [
+ 'invite-people',
+ {group: 'channels'},
+ {group: 'messaging'},
+ {group: 'calls'},
+ {group: 'teamsAndRoles'},
+ {group: 'integrations'},
+ {group: 'accessibility'},
+];
+
+// Files re-parented into groups — exclude from the orphan check.
+const COLLABORATE_HIDDEN = new Set([
+ 'channel-types', 'browse-channels', 'create-channels', 'join-leave-channels',
+ 'navigate-between-channels', 'channel-naming-conventions', 'channel-header-purpose',
+ 'rename-channels', 'archive-unarchive-channels', 'favorite-channels',
+ 'mark-channels-unread', 'manage-channel-members', 'manage-channel-bookmarks',
+ 'display-channel-banners', 'autotranslate-messages', 'convert-public-channels',
+ 'convert-group-messages',
+ 'send-messages', 'communicate-with-messages', 'reply-to-messages', 'organize-conversations',
+ 'format-messages', 'mark-messages-unread', 'mention-people', 'message-priority',
+ 'message-reminders', 'schedule-messages', 'save-pin-messages', 'flag-messages',
+ 'forward-messages', 'search-for-messages', 'share-links', 'share-files-in-messages',
+ 'react-with-emojis-gifs',
+ 'make-calls', 'audio-and-screensharing',
+ 'organize-using-teams', 'team-settings', 'organize-using-custom-user-groups',
+ 'learn-about-roles', 'invite-people',
+ 'extend-mattermost-with-integrations', 'agents-context-management',
+ 'collaborate-within-connected-microsoft-teams',
+ 'keyboard-shortcuts', 'team-keyboard-shortcuts', 'keyboard-accessibility',
+ 'view-system-information',
+]);
+
+// ---------------------------------------------------------------------------
+// Administration Guide — Onboard — manual grouping override.
+// ---------------------------------------------------------------------------
+//
+// Onboard is a flat 34-file dump spanning SSO/identity setup, guest/admin
+// permissions, user provisioning, and one-time migration tasks. All SSO and
+// identity-provider protocols (SAML, OIDC, Google, GitLab, Entra ID native,
+// OAuth->OIDC conversion, AD/LDAP) live under one "SSO & Identity" group;
+// SAML is nested as its own sub-category within it since it alone accounts
+// for 11 of those files (one per IdP plus FAQ/technical docs).
+
+const ADMIN_ONBOARD_GROUPS = {
+ sso: {
+ label: 'SSO & Identity',
+ items: [
+ {
+ label: 'SAML Single Sign-On',
+ landing: 'sso-saml',
+ items: [
+ 'sso-saml-before-you-begin',
+ 'sso-saml-adfs',
+ 'sso-saml-adfs-msws2016',
+ 'sso-saml-entraid',
+ 'sso-saml-keycloak',
+ 'sso-saml-ldapsync',
+ 'sso-saml-okta',
+ 'sso-saml-onelogin',
+ 'sso-saml-technical',
+ 'sso-saml-faq',
+ ],
+ },
+ 'sso-openidconnect',
+ 'sso-google',
+ 'sso-gitlab',
+ 'sso-entraid',
+ 'convert-oauth20-service-providers-to-openidconnect',
+ 'common-converting-oauth-to-openidconnect',
+ 'ad-ldap',
+ 'ad-ldap-groups-synchronization',
+ 'managing-team-channel-membership-using-ad-ldap-sync-groups',
+ ],
+ },
+ mfaCert: {
+ label: 'Multi-Factor & Certificate-Based Authentication',
+ items: [
+ 'multi-factor-authentication',
+ 'certificate-based-authentication',
+ 'ssl-client-certificate',
+ ],
+ },
+ userManagement: {
+ label: 'User Management',
+ items: [
+ 'guest-accounts',
+ 'delegated-granular-administration',
+ 'advanced-permissions',
+ 'advanced-permissions-backend-infrastructure',
+ ],
+ },
+ provisioning: {
+ label: 'User Provisioning & Bulk Data',
+ items: [
+ 'user-provisioning-workflows',
+ 'bulk-loading-data',
+ 'connected-workspaces',
+ ],
+ },
+ migration: {
+ label: 'Migrating to Mattermost',
+ landing: 'migrating-to-mattermost',
+ items: [
+ 'migrate-from-slack',
+ 'migrate-gitlab-omnibus',
+ 'migration-announcement-email',
+ ],
+ },
+};
+
+// Top-level Onboard order. Identity/auth setup first (SSO, then MFA/cert),
+// then user management, then provisioning/bulk data, then the one-time
+// migration tasks admins hit least often.
+const ADMIN_ONBOARD_ORDER = [
+ {group: 'sso'},
+ {group: 'mfaCert'},
+ {group: 'userManagement'},
+ {group: 'provisioning'},
+ {group: 'migration'},
+];
+
+// Files re-parented into groups — exclude from the orphan check. Every
+// Onboard file lives in a group (no standalone top-level docs), so this
+// covers all 34.
+const ADMIN_ONBOARD_HIDDEN = new Set([
+ 'sso-saml', 'sso-saml-before-you-begin', 'sso-saml-adfs', 'sso-saml-adfs-msws2016',
+ 'sso-saml-entraid', 'sso-saml-keycloak', 'sso-saml-ldapsync', 'sso-saml-okta',
+ 'sso-saml-onelogin', 'sso-saml-technical', 'sso-saml-faq',
+ 'sso-openidconnect', 'sso-google', 'sso-gitlab', 'sso-entraid',
+ 'convert-oauth20-service-providers-to-openidconnect', 'common-converting-oauth-to-openidconnect',
+ 'ad-ldap', 'ad-ldap-groups-synchronization', 'managing-team-channel-membership-using-ad-ldap-sync-groups',
+ 'multi-factor-authentication', 'certificate-based-authentication', 'ssl-client-certificate',
+ 'guest-accounts', 'delegated-granular-administration', 'advanced-permissions',
+ 'advanced-permissions-backend-infrastructure',
+ 'user-provisioning-workflows', 'bulk-loading-data', 'connected-workspaces',
+ 'migrating-to-mattermost', 'migrate-from-slack', 'migrate-gitlab-omnibus',
+ 'migration-announcement-email',
+]);
+
+// ---------------------------------------------------------------------------
+// Administration Guide — Scale — manual grouping override.
+// ---------------------------------------------------------------------------
+//
+// Scale was originally a flat 28-file dump mixing a whole run of
+// `scale-to-N-users` capacity-planning pages with unrelated HA, search, and
+// monitoring topics. In Sphinx's live nav, only the 7 monitoring/observability
+// pages below actually stay under Administration Guide — the other 21 files
+// (capacity planning, HA/architecture, search infrastructure, caching) are
+// listed under Deployment Guide → Reference Architecture instead (Sphinx
+// decouples toctree/nav placement from a page's physical file location, so
+// those files keep their `/administration-guide/scale/...` URLs there even
+// though they're navigated to from Deployment Guide). We mirror that split
+// here by physically moving those 21 files to
+// `deployment-guide/reference-architecture/scale/` (see the `scaling` group
+// in DEPLOYMENT_GROUPS), leaving only the 7 monitoring pages here. With just
+// one theme left, they're listed flat rather than wrapped in a redundant
+// "Observability & Monitoring" sub-category one level above itself.
+
+// Empty for now — kept (rather than removed) so buildAdminScaleItem's
+// `{group: '...'}` branch still throws a clear "unknown admin scale group"
+// error instead of a raw ReferenceError if a themed sub-group is needed here
+// again in the future (e.g. if Scale grows past this one theme).
+const ADMIN_SCALE_GROUPS = {};
+
+const ADMIN_SCALE_ORDER = [
+ 'deploy-prometheus-grafana-for-performance-monitoring',
+ 'collect-performance-metrics',
+ 'performance-monitoring-metrics',
+ 'performance-alerting',
+ 'deploy-grafana-loki-for-centralized-logging',
+ 'push-notification-health-targets',
+ 'ensuring-releases-perform-at-scale',
+];
+
+// Files re-parented into groups — exclude from the orphan check. Empty now
+// that Scale's 7 remaining files are listed flat (directly known via
+// ADMIN_SCALE_ORDER) rather than nested inside a sub-group.
+const ADMIN_SCALE_HIDDEN = new Set([]);
+
// ---------------------------------------------------------------------------
// Integrations Guide — manual grouping override.
// ---------------------------------------------------------------------------
@@ -1035,9 +1360,115 @@ function regroupAdminManage(manageCat) {
return manageCat;
}
+function buildAdminOnboardItem(spec, leafLabels) {
+ if (typeof spec === 'string') {
+ const id = `administration-guide/onboard/${spec}`;
+ return {type: 'doc', id, label: leafLabels[id] || humanize(spec.split('/').pop())};
+ }
+ if (spec.group) {
+ const g = ADMIN_ONBOARD_GROUPS[spec.group];
+ if (!g) throw new Error(`unknown admin onboard group: ${spec.group}`);
+ return buildAdminOnboardGroup(g, leafLabels);
+ }
+ return buildAdminOnboardGroup(spec, leafLabels);
+}
+
+function buildAdminOnboardGroup(g, leafLabels) {
+ const items = g.items.map((it) => buildAdminOnboardItem(it, leafLabels));
+ const cat = {type: 'category', label: g.label, collapsed: true, items};
+ if (g.landing) cat.link = {type: 'doc', id: `administration-guide/onboard/${g.landing}`};
+ return cat;
+}
+
+// Replace the auto-generated "Onboard" sub-category's items (in place,
+// preserving its position among Administration Guide's other sub-categories)
+// with the manual grouping above.
+function regroupAdminOnboard(onboardCat) {
+ const leafLabels = collectLeafLabels(onboardCat);
+ const items = ADMIN_ONBOARD_ORDER.map((spec) => buildAdminOnboardItem(spec, leafLabels));
+
+ const known = new Set();
+ (function walk(n) {
+ if (Array.isArray(n)) n.forEach(walk);
+ else if (n && typeof n === 'object') {
+ if (n.type === 'doc' && n.id) known.add(n.id);
+ if (n.link && n.link.id) known.add(n.link.id);
+ if (n.items) walk(n.items);
+ }
+ })(items);
+ const hiddenIds = new Set();
+ for (const h of ADMIN_ONBOARD_HIDDEN) hiddenIds.add(`administration-guide/onboard/${h}`);
+ const orphans = [];
+ for (const id of Object.keys(leafLabels)) {
+ if (!known.has(id) && !hiddenIds.has(id)) orphans.push(id);
+ }
+ if (orphans.length > 0) {
+ console.warn(`[sidebar] WARN: ${orphans.length} Onboard file(s) missing from ADMIN_ONBOARD_ORDER — falling through to root:`);
+ for (const id of orphans) console.warn(` - ${id}`);
+ for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
+ }
+
+ onboardCat.items = items;
+ return onboardCat;
+}
+
+function buildAdminScaleItem(spec, leafLabels) {
+ if (typeof spec === 'string') {
+ const id = `administration-guide/scale/${spec}`;
+ return {type: 'doc', id, label: leafLabels[id] || humanize(spec.split('/').pop())};
+ }
+ if (spec.group) {
+ const g = ADMIN_SCALE_GROUPS[spec.group];
+ if (!g) throw new Error(`unknown admin scale group: ${spec.group}`);
+ return buildAdminScaleGroup(g, leafLabels);
+ }
+ return buildAdminScaleGroup(spec, leafLabels);
+}
+
+function buildAdminScaleGroup(g, leafLabels) {
+ const items = g.items.map((it) => buildAdminScaleItem(it, leafLabels));
+ const cat = {type: 'category', label: g.label, collapsed: true, items};
+ if (g.landing) cat.link = {type: 'doc', id: `administration-guide/scale/${g.landing}`};
+ return cat;
+}
+
+// Replace the auto-generated "Scale" sub-category's items (in place,
+// preserving its position among Administration Guide's other sub-categories)
+// with the manual grouping above.
+function regroupAdminScale(scaleCat) {
+ const leafLabels = collectLeafLabels(scaleCat);
+ const items = ADMIN_SCALE_ORDER.map((spec) => buildAdminScaleItem(spec, leafLabels));
+
+ const known = new Set();
+ (function walk(n) {
+ if (Array.isArray(n)) n.forEach(walk);
+ else if (n && typeof n === 'object') {
+ if (n.type === 'doc' && n.id) known.add(n.id);
+ if (n.link && n.link.id) known.add(n.link.id);
+ if (n.items) walk(n.items);
+ }
+ })(items);
+ const hiddenIds = new Set();
+ for (const h of ADMIN_SCALE_HIDDEN) hiddenIds.add(`administration-guide/scale/${h}`);
+ const orphans = [];
+ for (const id of Object.keys(leafLabels)) {
+ if (!known.has(id) && !hiddenIds.has(id)) orphans.push(id);
+ }
+ if (orphans.length > 0) {
+ console.warn(`[sidebar] WARN: ${orphans.length} Scale file(s) missing from ADMIN_SCALE_ORDER — falling through to root:`);
+ for (const id of orphans) console.warn(` - ${id}`);
+ for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
+ }
+
+ scaleCat.items = items;
+ return scaleCat;
+}
+
function buildAdminGuideSidebar(autoCat) {
let foundConfigure = false;
let foundManage = false;
+ let foundOnboard = false;
+ let foundScale = false;
for (const it of autoCat.items) {
if (it.type !== 'category') continue;
let dirName = null;
@@ -1054,6 +1485,12 @@ function buildAdminGuideSidebar(autoCat) {
} else if (dirName === 'manage') {
regroupAdminManage(it);
foundManage = true;
+ } else if (dirName === 'onboard') {
+ regroupAdminOnboard(it);
+ foundOnboard = true;
+ } else if (dirName === 'scale') {
+ regroupAdminScale(it);
+ foundScale = true;
}
}
if (!foundConfigure) {
@@ -1062,9 +1499,74 @@ function buildAdminGuideSidebar(autoCat) {
if (!foundManage) {
console.warn('[sidebar] WARN: Administration Guide "Manage" sub-category not found — ADMIN_MANAGE_GROUPS override was not applied.');
}
+ if (!foundOnboard) {
+ console.warn('[sidebar] WARN: Administration Guide "Onboard" sub-category not found — ADMIN_ONBOARD_GROUPS override was not applied.');
+ }
+ if (!foundScale) {
+ console.warn('[sidebar] WARN: Administration Guide "Scale" sub-category not found — ADMIN_SCALE_GROUPS override was not applied.');
+ }
return autoCat;
}
+// ---------------------------------------------------------------------------
+// End User Guide — builder (regroups the "Collaborate" sub-category).
+// ---------------------------------------------------------------------------
+
+function buildCollaborateItem(spec, leafLabels) {
+ if (typeof spec === 'string') {
+ const id = `end-user-guide/collaborate/${spec}`;
+ return {type: 'doc', id, label: leafLabels[id] || humanize(spec)};
+ }
+ if (spec.items) {
+ // Inline subgroup (no COLLABORATE_GROUPS lookup) — mirrors
+ // buildAdminManageItem/buildAdminManageGroup, so a group's items can
+ // nest a further {label, items} sub-group for a 4th nesting level.
+ return buildCollaborateGroup(spec, leafLabels);
+ }
+ const g = COLLABORATE_GROUPS[spec.group];
+ if (!g) throw new Error(`unknown collaborate group: ${spec.group}`);
+ return buildCollaborateGroup(g, leafLabels);
+}
+
+function buildCollaborateGroup(g, leafLabels) {
+ const items = g.items.map((it) => buildCollaborateItem(it, leafLabels));
+ const cat = {type: 'category', label: g.label, collapsed: true, items};
+ if (g.landing) cat.link = {type: 'doc', id: `end-user-guide/collaborate/${g.landing}`};
+ return cat;
+}
+
+// Replace the auto-generated "Collaborate" sub-category's items (in place,
+// preserving its position among End User Guide's other sub-categories) with
+// the manual grouping above — same pattern as regroupAdminConfigure/Manage.
+function regroupCollaborate(collaborateCat) {
+ const leafLabels = collectLeafLabels(collaborateCat);
+ const items = COLLABORATE_ORDER.map((spec) => buildCollaborateItem(spec, leafLabels));
+
+ const known = new Set();
+ (function walk(n) {
+ if (Array.isArray(n)) n.forEach(walk);
+ else if (n && typeof n === 'object') {
+ if (n.type === 'doc' && n.id) known.add(n.id);
+ if (n.link && n.link.id) known.add(n.link.id);
+ if (n.items) walk(n.items);
+ }
+ })(items);
+ const hiddenIds = new Set();
+ for (const h of COLLABORATE_HIDDEN) hiddenIds.add(`end-user-guide/collaborate/${h}`);
+ const orphans = [];
+ for (const id of Object.keys(leafLabels)) {
+ if (!known.has(id) && !hiddenIds.has(id)) orphans.push(id);
+ }
+ if (orphans.length > 0) {
+ console.warn(`[sidebar] WARN: ${orphans.length} Collaborate file(s) missing from COLLABORATE_ORDER — falling through to root:`);
+ for (const id of orphans) console.warn(` - ${id}`);
+ for (const id of orphans) items.push({type: 'doc', id, label: leafLabels[id]});
+ }
+
+ collaborateCat.items = items;
+ return collaborateCat;
+}
+
// ---------------------------------------------------------------------------
// Integrations Guide — builder.
// ---------------------------------------------------------------------------
@@ -1126,11 +1628,16 @@ function buildIntegrationsSidebar(autoCat) {
}
// ---------------------------------------------------------------------------
-// End User Guide — nests the Agents plugin's usage-tips page under the
-// existing "AI Agents" doc, the same way Configure nests Agents' admin-side
-// pages (see ADMIN_CONFIGURE_GROUPS.agents above). End User Guide is
-// otherwise fully filesystem-driven, so this is a narrow, targeted
-// promotion rather than a full manual-grouping override.
+// End User Guide — builder. Two independent overrides on top of the
+// otherwise filesystem-driven auto-generated sidebar:
+// 1. Nests the Agents plugin's usage-tips page under the existing "AI
+// Agents" doc, the same way Configure nests Agents' admin-side pages
+// (see ADMIN_CONFIGURE_GROUPS.agents above) — a narrow, targeted
+// promotion rather than a full manual-grouping override.
+// 2. Regroups the "Collaborate" sub-category (49 files) into the topic
+// groups defined in COLLABORATE_GROUPS above, the same
+// manual-grouping-override pattern used for Administration Guide's
+// Configure/Manage/Onboard/Scale sections.
// ---------------------------------------------------------------------------
// Finds the {type: 'doc', id: docId} leaf anywhere in `items` and replaces
@@ -1162,6 +1669,27 @@ function buildEndUserGuideSidebar(autoCat) {
if (!promoted) {
console.warn('[sidebar] WARN: End User Guide "agents" doc not found — Agents usage-tips nesting was not applied.');
}
+
+ let foundCollaborate = false;
+ for (const it of autoCat.items) {
+ if (it.type !== 'category') continue;
+ let dirName = null;
+ if (it.link && it.link.id) {
+ dirName = it.link.id.split('/')[1];
+ }
+ if (!dirName && it.items) {
+ const firstDoc = it.items.find((c) => c.type === 'doc' && c.id);
+ if (firstDoc) dirName = firstDoc.id.split('/')[1];
+ }
+ if (dirName === 'collaborate') {
+ regroupCollaborate(it);
+ foundCollaborate = true;
+ }
+ }
+ if (!foundCollaborate) {
+ console.warn('[sidebar] WARN: End User Guide "Collaborate" sub-category not found — COLLABORATE_GROUPS override was not applied.');
+ }
+
return autoCat;
}
@@ -1186,6 +1714,8 @@ function main() {
cat = buildDeploymentSidebar(cat);
} else if (dir === 'administration-guide') {
cat = buildAdminGuideSidebar(cat);
+ } else if (dir === 'end-user-guide') {
+ cat = buildEndUserGuideSidebar(cat);
} else if (dir === 'integrations-guide') {
cat = buildIntegrationsSidebar(cat);
} else if (dir === 'end-user-guide') {
diff --git a/docs/site/sidebars/active-redirects.json b/docs/site/sidebars/active-redirects.json
index 05ceebe0ae5d..548f3c91b460 100644
--- a/docs/site/sidebars/active-redirects.json
+++ b/docs/site/sidebars/active-redirects.json
@@ -565,11 +565,11 @@
},
{
"from": "/administration-guide/configure/calls-deployment",
- "to": "/administration-guide/configure/calls-deployment-guide"
+ "to": "/deployment-guide/calls/calls-deployment-guide"
},
{
"from": "/administration-guide/configure/calls-overview",
- "to": "/administration-guide/configure/calls-deployment-guide"
+ "to": "/deployment-guide/calls/calls-deployment-guide"
},
{
"from": "/channels/find-channels",
@@ -777,19 +777,19 @@
},
{
"from": "/configure/calls-deployment",
- "to": "/administration-guide/configure/calls-deployment-guide"
+ "to": "/deployment-guide/calls/calls-deployment-guide"
},
{
"from": "/configure/calls-log-collection",
- "to": "/administration-guide/configure/calls-logging"
+ "to": "/deployment-guide/calls/calls-logging"
},
{
"from": "/configure/calls-overview",
- "to": "/administration-guide/configure/calls-deployment-guide"
+ "to": "/deployment-guide/calls/calls-deployment-guide"
},
{
"from": "/configure/calls-troubleshooting",
- "to": "/administration-guide/configure/calls-logging"
+ "to": "/deployment-guide/calls/calls-logging"
},
{
"from": "/configure/cloud-billing-account-settings",
@@ -1149,11 +1149,11 @@
},
{
"from": "/deployment/elasticsearch",
- "to": "/administration-guide/scale/elasticsearch-setup"
+ "to": "/deployment-guide/reference-architecture/scale/elasticsearch-setup"
},
{
"from": "/deployment/enterprise-deployment-guide",
- "to": "/administration-guide/scale/scaling-for-enterprise"
+ "to": "/deployment-guide/reference-architecture/scale/scaling-for-enterprise"
},
{
"from": "/deployment/guest-accounts",
@@ -1161,7 +1161,7 @@
},
{
"from": "/deployment/ha",
- "to": "/administration-guide/scale/high-availability-cluster-based-deployment"
+ "to": "/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment"
},
{
"from": "/deployment/metrics",
@@ -1181,7 +1181,7 @@
},
{
"from": "/deployment/scaling",
- "to": "/administration-guide/scale/scaling-for-enterprise"
+ "to": "/deployment-guide/reference-architecture/scale/scaling-for-enterprise"
},
{
"from": "/deployment/sso-ldap",
@@ -2413,7 +2413,7 @@
},
{
"from": "/scale/additional-ha-considerations",
- "to": "/administration-guide/scale/additional-ha-considerations"
+ "to": "/deployment-guide/reference-architecture/scale/additional-ha-considerations"
},
{
"from": "/scale/collect-performance-metrics",
@@ -2421,7 +2421,7 @@
},
{
"from": "/scale/common-configure-mattermost-for-enterprise-search",
- "to": "/administration-guide/scale/common-configure-mattermost-for-enterprise-search"
+ "to": "/deployment-guide/reference-architecture/scale/common-configure-mattermost-for-enterprise-search"
},
{
"from": "/scale/deploy-prometheus-grafana-for-performance-monitoring",
@@ -2429,7 +2429,7 @@
},
{
"from": "/scale/elasticsearch-setup",
- "to": "/administration-guide/scale/elasticsearch-setup"
+ "to": "/deployment-guide/reference-architecture/scale/elasticsearch-setup"
},
{
"from": "/scale/ensuring-releases-perform-at-scale",
@@ -2437,23 +2437,23 @@
},
{
"from": "/scale/enterprise-search",
- "to": "/administration-guide/scale/enterprise-search"
+ "to": "/deployment-guide/reference-architecture/scale/enterprise-search"
},
{
"from": "/scale/estimated-storage-per-user-per-month",
- "to": "/administration-guide/scale/estimated-storage-per-user-per-month"
+ "to": "/deployment-guide/reference-architecture/scale/estimated-storage-per-user-per-month"
},
{
"from": "/scale/high-availability-cluster-based-deployment",
- "to": "/administration-guide/scale/high-availability-cluster-based-deployment"
+ "to": "/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment"
},
{
"from": "/scale/lifetime-storage",
- "to": "/administration-guide/scale/lifetime-storage"
+ "to": "/deployment-guide/reference-architecture/scale/lifetime-storage"
},
{
"from": "/scale/opensearch-setup",
- "to": "/administration-guide/scale/opensearch-setup"
+ "to": "/deployment-guide/reference-architecture/scale/opensearch-setup"
},
{
"from": "/scale/performance-alerting",
@@ -2473,79 +2473,79 @@
},
{
"from": "/scale/redis",
- "to": "/administration-guide/scale/redis"
+ "to": "/deployment-guide/reference-architecture/scale/redis"
},
{
"from": "/scale/scale-to-100000-users",
- "to": "/administration-guide/scale/scale-to-100000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-100000-users"
},
{
"from": "/scale/scale-to-15000-users",
- "to": "/administration-guide/scale/scale-to-15000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-15000-users"
},
{
"from": "/scale/scale-to-200-users",
- "to": "/administration-guide/scale/scale-to-200-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-200-users"
},
{
"from": "/scale/scale-to-2000-users",
- "to": "/administration-guide/scale/scale-to-2000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-2000-users"
},
{
"from": "/scale/scale-to-200000-users",
- "to": "/administration-guide/scale/scale-to-200000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-200000-users"
},
{
"from": "/scale/scale-to-30000-users",
- "to": "/administration-guide/scale/scale-to-30000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-30000-users"
},
{
"from": "/scale/scale-to-50000-users",
- "to": "/administration-guide/scale/scale-to-50000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-50000-users"
},
{
"from": "/scale/scale-to-80000-users",
- "to": "/administration-guide/scale/scale-to-80000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-80000-users"
},
{
"from": "/scale/scale-to-90000-users",
- "to": "/administration-guide/scale/scale-to-90000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-90000-users"
},
{
"from": "/scale/scaling-for-enterprise",
- "to": "/administration-guide/scale/scaling-for-enterprise"
+ "to": "/deployment-guide/reference-architecture/scale/scaling-for-enterprise"
},
{
"from": "/scale/high-availability-cluster",
- "to": "/administration-guide/scale/high-availability-cluster-based-deployment"
+ "to": "/deployment-guide/reference-architecture/scale/high-availability-cluster-based-deployment"
},
{
"from": "/scale/scale-to-100-users",
- "to": "/administration-guide/scale/scale-to-200-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-200-users"
},
{
"from": "/scale/scale-to-1000-users",
- "to": "/administration-guide/scale/scale-to-2000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-2000-users"
},
{
"from": "/scale/scale-to-25000-users",
- "to": "/administration-guide/scale/scale-to-30000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-30000-users"
},
{
"from": "/scale/scale-to-70000-users",
- "to": "/administration-guide/scale/scale-to-80000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-80000-users"
},
{
"from": "/scale/scale-to-79000-users",
- "to": "/administration-guide/scale/scale-to-80000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-80000-users"
},
{
"from": "/scale/scale-to-88000-users",
- "to": "/administration-guide/scale/scale-to-90000-users"
+ "to": "/deployment-guide/reference-architecture/scale/scale-to-90000-users"
},
{
"from": "/scale/elasticsearch",
- "to": "/administration-guide/scale/elasticsearch-setup"
+ "to": "/deployment-guide/reference-architecture/scale/elasticsearch-setup"
},
{
"from": "/guides/agents",
@@ -2774,6 +2774,30 @@
{
"from": "/product-overview/product-overview-index",
"to": "/"
+ },
+ {
+ "from": "/administration-guide/configure/calls-deployment-guide",
+ "to": "/deployment-guide/calls/calls-deployment-guide"
+ },
+ {
+ "from": "/administration-guide/configure/calls-rtcd-setup",
+ "to": "/deployment-guide/calls/calls-rtcd-setup"
+ },
+ {
+ "from": "/administration-guide/configure/calls-offloader-setup",
+ "to": "/deployment-guide/calls/calls-offloader-setup"
+ },
+ {
+ "from": "/administration-guide/configure/calls-kubernetes",
+ "to": "/deployment-guide/calls/calls-kubernetes"
+ },
+ {
+ "from": "/administration-guide/configure/calls-logging",
+ "to": "/deployment-guide/calls/calls-logging"
+ },
+ {
+ "from": "/administration-guide/configure/calls-metrics-monitoring",
+ "to": "/deployment-guide/calls/calls-metrics-monitoring"
}
]
-}
\ No newline at end of file
+}
diff --git a/docs/site/src/css/custom.css b/docs/site/src/css/custom.css
index 5bb89ca7019f..8c12f0df50e4 100644
--- a/docs/site/src/css/custom.css
+++ b/docs/site/src/css/custom.css
@@ -99,6 +99,21 @@
--ifm-color-content-secondary: var(--mm-text-secondary);
}
+/* The mobile nav slide-out (.navbar-sidebar) always has a dark denim
+ * background, but its menu inherits the light-mode --ifm-menu-color*
+ * vars tuned for the light desktop sidebar — force the dark-mode values
+ * unconditionally so mobile text stays readable in light mode too. */
+.navbar-sidebar {
+ --ifm-menu-color: rgba(255,255,255,0.78);
+ --ifm-menu-color-active: var(--mm-color-white);
+ --ifm-menu-color-background-active: var(--mm-denim-700);
+ --ifm-menu-color-background-hover: var(--mm-denim-700);
+}
+/* "Back to main menu" text color isn't covered by the vars above. */
+.navbar-sidebar__back {
+ color: var(--ifm-menu-color-active);
+}
+
/* === Type rhythm === */
html {
@@ -339,6 +354,13 @@ h2, h3, h4 {
padding-bottom: 0.4rem;
}
+/* Category headers hardcode --mm-text-secondary above, so they don't
+ * pick up the .navbar-sidebar menu-color override — fix separately. */
+.navbar-sidebar .menu .menu__list-item-collapsible > .menu__link,
+.navbar-sidebar .menu > .menu__list > .menu__list-item > .menu__link {
+ color: var(--ifm-menu-color);
+}
+
/* Endpoint leaves keep their natural case (e.g. "Login to Mattermost server"). */
.menu .menu__link.api-method {
font-family: var(--mm-font-sans);
diff --git a/e2e-tests/README.md b/e2e-tests/README.md
index 86e403d4664b..f880a98dd6ca 100644
--- a/e2e-tests/README.md
+++ b/e2e-tests/README.md
@@ -8,6 +8,8 @@ This directory contains the E2E testing code for the Mattermost web client.
Please refer to the [dedicated developer documentation](https://developers.mattermost.com/contribute/more-info/webapp/e2e-testing/) for instructions.
+> **Playwright note:** the instructions below describe the Docker Compose flow Cypress uses (and that Playwright previously used too). Playwright's CI and recommended local setup have since moved to [Testcontainers](https://node.testcontainers.org/) — see `playwright/README.md`'s Server Setup section for details. This Compose flow remains the way to run Cypress, and to run a plain server instance (`TEST=none make`).
+
##### For pipeline debugging
The E2E testing pipeline's scripts depend on the following tools being installed on your system: `docker`, `docker-compose`, `make`, `git`, `jq`, `node`, and some common utilities (`coreutils`, `findutils`, `bash`, `awk`, `sed`, `grep`)
@@ -22,7 +24,7 @@ Instructions, detailed:
* `ENABLED_DOCKER_SERVICES`: a space-separated list of services to start alongside the server. Default to `postgres inbucket`, for smoke test purposes and for lightweight and faster start-up time. Depending on the test requirement being worked on, you may want to override as needed, as such:
- Cypress full tests require all services to be running: `postgres inbucket minio openldap elasticsearch keycloak`.
- Cypress smoke tests require only the following: `postgres inbucket`.
- - Playwright full tests require only the following: `postgres inbucket`.
+ - Playwright no longer runs against this Compose flow in CI (see the note above) — this only matters if you're using it to spin up a plain server instance (`TEST=none make`) for Playwright's `external` mode.
* The following variables, will be passed over to the server container: `MM_LICENSE` (no enterprise features will be available if this is unset; required when `SERVER=cloud`), and the exploded `MM_ENV` (a comma-separated list of env var specifications)
* The following variables, which will be passed over to the cypress container: `BRANCH`, `BUILD_ID`, `CI_BASE_URL`, `BROWSER`, `AUTOMATION_DASHBOARD_URL` and `AUTOMATION_DASHBOARD_TOKEN`
* The `SERVER_IMAGE` variable can also be set if you want to select a custom mattermost-server image. If not specified, the value of the `SERVER_IMAGE_DEFAULT` variable defined in file `.ci/.e2erc` is used.
@@ -39,7 +41,7 @@ Instructions, detailed:
3. `make`: start and prepare the server, then run the Cypress smoke tests
* You can track the progress of the run in the `http://localhost:4000/cycles` dashboard if you launched it locally
* For `SERVER=cloud` runs, you'll need to first create a cloud customer against the specified `CWS_URL` service by running `make cloud-init`. The user isn't automatically removed, and may be reused across multiple runs until you run `make cloud-teardown` to delete it.
- * If you want to run the Playwright tests instead of the Cypress ones, you can run `TEST=playwright make`
+ * If you want to run the Playwright tests instead of the Cypress ones, you can run `TEST=playwright make` — though `playwright/README.md`'s Testcontainers option is now the recommended way to run Playwright locally/in CI
* If you just want to run a local server instance, without any further testing, you can run `TEST=none make`
* If you're using the automation dashboard, you have the option of sharding the E2E test run: you can launch the `make` command in parallel on different machines (NB: you must use the same `BUILD_ID` and `BRANCH` values that you used for `make generate-test-cycle`) to distribute running the test cases across them. When doing this, you should also set on each machine the `CI_BASE_URL` variable to a value that uniquely identifies the instance where `make` is running.
* This script will also parse the local test results, and write a `e2e-tests/${TEST}/results/summary.json` file containing the following keys: `passed`, `failed` and `failed_expected` (the total number of testcases that were run is the sum of these three numbers)
@@ -71,4 +73,4 @@ For Cypress:
* Your system needs to be setup for Cypress usage, to be able to run this command. Refer to the [E2E testing developer documentation](https://developers.mattermost.com/contribute/more-info/webapp/e2e-testing/) for this.
4. The `cypress/results/testPasses.json` file will count, for each of the testfiles, how many times it was run, and how many times each of the testcases contained in it passed. If the attempts and passes numbers do not match, that specific testcase may be flaky.
-For Playwright: WIP
+For Playwright: not currently supported — Playwright tests are run and re-run individually via `npm run test -- `, see `playwright/README.md`.
diff --git a/e2e-tests/playwright/.gitignore b/e2e-tests/playwright/.gitignore
index 425c31b423ef..88f72916522e 100644
--- a/e2e-tests/playwright/.gitignore
+++ b/e2e-tests/playwright/.gitignore
@@ -14,3 +14,6 @@ test/.eslintcache
# build
dist
*.tsbuildinfo
+
+# testcontainers ("full" mode) — generated per-run, see docs/testcontainers/testcontainers_plan.md
+.env.testcontainers
diff --git a/e2e-tests/playwright/CLAUDE.OPTIONAL.md b/e2e-tests/playwright/CLAUDE.OPTIONAL.md
index f144eb38ff87..a99d4fb09f3c 100644
--- a/e2e-tests/playwright/CLAUDE.OPTIONAL.md
+++ b/e2e-tests/playwright/CLAUDE.OPTIONAL.md
@@ -91,6 +91,7 @@ npm run show-report
- Component abstractions for UI elements
- Test utilities and fixtures
- Server setup and management functions
+ - Testcontainers orchestration (`lib/src/containers/`) for `testcontainers` mode (see Server Setup below)
- Visual testing support
2. **`specs/` Directory**: Contains the actual test files organized by type:
@@ -135,6 +136,11 @@ Tests can be configured through environment variables:
- `PW_SLOWMO` - Add delay between actions in ms (default: 0)
- `PW_WORKERS` - Number of parallel workers (default: 1)
- `PERCY_TOKEN` - Authentication token for Percy visual testing service (required for Percy tests)
+- `PW_USE_TESTCONTAINERS` - Selects `testcontainers` mode, see Server Setup below (default: false)
+- `PW_TESTCONTAINERS_SERVICES` - Comma-separated additional services to start, e.g. `minio,openldap` (default: `minio,openldap,keycloak,elasticsearch`; set to an empty string to start none)
+- `PW_TESTCONTAINERS_REUSE` - Reuse containers across repeated local runs instead of recreating them; tear down explicitly with `npm run testcontainers:down` (default: true)
+- `SERVER_IMAGE` - Mattermost server image `testcontainers` mode starts (default: `mattermostdevelopment/mattermost-enterprise-edition:master`)
+- `MM_ENV` - Comma-separated `KEY=VALUE` server config overrides for `testcontainers` mode (default: none)
## Server Setup
@@ -146,10 +152,12 @@ Before running tests, a Mattermost server must be available. Two options:
cd server && make run
```
-2. **Run using Docker** (recommended for testing):
+2. **Testcontainers** (recommended for testing, and what CI uses) — no separate step needed, Playwright starts and tears down the server + dependencies itself:
```bash
- # Configure environment in e2e-tests/.ci/env
- cd e2e-tests && TEST=playwright make
+ PW_USE_TESTCONTAINERS=true npm run test
+ # Starts minio,openldap,keycloak,elasticsearch by default; add PW_TESTCONTAINERS_SERVICES=... to change (e.g. add opensearch,azurite, or set to "" to start none)
+ # Containers are reused across runs by default; tear down with `npm run testcontainers:down` (or set PW_TESTCONTAINERS_REUSE=false for a self-cleaning one-off run)
+ # `npm run testcontainers:up` brings the stack up (or confirms an existing one) without running any tests
```
## Best Practices
diff --git a/e2e-tests/playwright/README.md b/e2e-tests/playwright/README.md
index 1581224c6915..42b4f1781c3f 100644
--- a/e2e-tests/playwright/README.md
+++ b/e2e-tests/playwright/README.md
@@ -17,30 +17,27 @@ cd webapp && make run
cd server && make run-server
```
-**Option 2: Run using Docker (recommended for testing)**
+**Option 2: Testcontainers (recommended for testing, and what CI uses)**
-```bash
-# 1. Configure environment variables in e2e-tests/.ci/env
-# Create this file if it doesn't exist
+No separate terminal or setup step needed — Playwright brings up Postgres, Inbucket, and the Mattermost server itself via [Testcontainers](https://node.testcontainers.org/), then tears them down after the run.
-# 2. Set the server image (optional)
-# To use the latest master image:
-SERVER_IMAGE="mattermostdevelopment/mattermost-enterprise-edition:master"
-# If not set, it will use the current commit: mattermostdevelopment/mattermost-enterprise-edition:$(git rev-parse --short=7 HEAD)
-# Note: The image must exist in Docker Hub at https://hub.docker.com/r/mattermostdevelopment/mattermost-enterprise-edition/tags
+```bash
+# Run with defaults (Postgres, Inbucket, Mattermost server, minio, openldap, keycloak, elasticsearch)
+PW_USE_TESTCONTAINERS=true npm run test -- login
-# 3. Add your license if needed
-MM_LICENSE=
+# Change which additional services start, comma-separated (or "" to start none)
+PW_USE_TESTCONTAINERS=true PW_TESTCONTAINERS_SERVICES=minio,openldap npm run test
-# 4. For additional configuration options, see e2e-tests/README.md
+# Pin a specific server image (defaults to mattermostdevelopment/mattermost-enterprise-edition:master)
+PW_USE_TESTCONTAINERS=true SERVER_IMAGE=mattermostdevelopment/mattermost-enterprise-edition: npm run test
-# 5. Run the server and Playwright's smoke tests from the e2e-tests directory
-cd e2e-tests && TEST=playwright make
+# Pass arbitrary MM_* config overrides as comma-separated KEY=VALUE pairs
+PW_USE_TESTCONTAINERS=true MM_ENV=MM_LICENSE= npm run test
```
-This approach uses the server's Docker image to create a consistent testing environment. It automatically configures the server with the necessary settings for Playwright tests and handles dependencies.
+Containers are reused across invocations by default (`PW_TESTCONTAINERS_REUSE=true`) instead of being recreated every run — tear the stack down explicitly when you're done with `npm run testcontainers:down`. Set `PW_TESTCONTAINERS_REUSE=false` for a one-off run that tears itself down when it finishes. Use `npm run testcontainers:up` to just bring the stack up (or confirm an existing one's still reachable) without running any tests.
-`ENABLED_DOCKER_SERVICES` defaults to the base `postgres inbucket` set (plus the auto-added `playwright` and `webhook-interactions` services). Expand it only when a test needs an extra dependency (for example `minio` for file storage or `openldap` for LDAP).
+See `lib/README.md` for every available environment variable.
#### 2. Install dependencies and run the test.
diff --git a/e2e-tests/playwright/eslint.config.mjs b/e2e-tests/playwright/eslint.config.mjs
index 3a0d46a430d4..e0d9cbb6f726 100644
--- a/e2e-tests/playwright/eslint.config.mjs
+++ b/e2e-tests/playwright/eslint.config.mjs
@@ -7,7 +7,14 @@ import eslintPlugin from '@mattermost/eslint-plugin';
export default [
{
- ignores: ['**/node_modules', '**/dist', '**/playwright-report', '**/test-results', '**/results'],
+ ignores: [
+ '**/node_modules',
+ '**/dist',
+ '**/playwright-report',
+ '**/test-results',
+ '**/results',
+ 'lib/src/containers/assets/webhook/tests/**',
+ ],
},
...eslintPlugin.configs.base,
{
diff --git a/e2e-tests/playwright/global_setup.ts b/e2e-tests/playwright/global_setup.ts
index 6729ac81b31a..a7bfebcae9c5 100644
--- a/e2e-tests/playwright/global_setup.ts
+++ b/e2e-tests/playwright/global_setup.ts
@@ -1,21 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {baseGlobalSetup, testConfig} from '@mattermost/playwright-lib';
+import chalk from 'chalk';
+
+import {baseGlobalSetup, startStack, stopStack, testConfig} from '@mattermost/playwright-lib';
async function globalSetup() {
try {
+ // With PW_USE_TESTCONTAINERS=true, bring up the server + dependencies via Testcontainers
+ // before pinging it. No-op otherwise, when a server is expected to already be running.
+ await startStack();
await baseGlobalSetup();
} catch (error: unknown) {
// eslint-disable-next-line no-console
- console.error(error);
- throw new Error(
- `Global setup failed.\n\tEnsure the server at ${testConfig.baseURL} is running and accessible.\n\tPlease check the logs for more details.`,
- );
+ console.error(chalk.cyan('[testcontainers]'), error);
+ // Whatever startStack() managed to bring up (e.g. baseGlobalSetup() failed after the
+ // stack itself came up fine) shouldn't linger just because we're about to throw.
+ await stopStack();
+ const message = error instanceof Error ? error.message : String(error);
+ const hint = testConfig.useTestContainers
+ ? 'Check the container named above and its logs under logs/.'
+ : `Ensure the server at ${testConfig.baseURL} is running and accessible.`;
+ throw new Error(chalk.red(`[testcontainers] Global setup failed: ${message}\n${hint}`));
}
- return function () {
- // placeholder for teardown setup
+ return async function () {
+ await stopStack();
};
}
diff --git a/e2e-tests/playwright/lib/README.md b/e2e-tests/playwright/lib/README.md
index 61056aa2ebab..0cfed5bfe00e 100644
--- a/e2e-tests/playwright/lib/README.md
+++ b/e2e-tests/playwright/lib/README.md
@@ -139,6 +139,27 @@ All environment variables are optional with sensible defaults.
| -------- | ------------------------------------ | ------- |
| `CI` | Set automatically in CI environments | N/A |
+#### Testcontainers
+
+Selects `testcontainers` mode — Playwright brings up the server + dependencies itself via [Testcontainers](https://node.testcontainers.org/) — instead of the `external` mode default, which expects a server already running at `PW_BASE_URL`.
+
+| Variable | Description | Default |
+| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
+| `PW_USE_TESTCONTAINERS` | Selects `testcontainers` mode | `false` |
+| `PW_TESTCONTAINERS_SERVICES` | Comma-separated additional services to start (`openldap`, `keycloak`, `elasticsearch`, `opensearch`, `minio`, `azurite`); set to an empty string to start none | `minio,openldap,keycloak,elasticsearch` |
+| `PW_TESTCONTAINERS_REUSE` | Reuse containers across repeated local runs instead of recreating them; tear down with `npm run testcontainers:down` (requires `testcontainers.reuse.enable=true` in `~/.testcontainers.properties`) | `true` |
+| `PW_TESTCONTAINERS_CONTAINER_RUNNER` | Set when the Playwright process itself runs inside a Docker container (e.g. CI), to join it to the Testcontainers network | `false` |
+| `SERVER_IMAGE` | Prebuilt Mattermost server image `testcontainers` mode starts | `mattermostdevelopment/mattermost-enterprise-edition:master` |
+| `MM_ENV` | Comma-separated `KEY=VALUE` server config overrides, merged over the test baseline | none |
+| `PW_LDAP_HOST` / `PW_LDAP_PORT` | OpenLDAP host/port (only used when `openldap` is started) | `localhost` / `389` |
+| `PW_KEYCLOAK_URL` | Keycloak URL (only used when `keycloak` is started) | `http://localhost:8484` |
+| `PW_ELASTICSEARCH_URL` | Elasticsearch URL (only used when `elasticsearch` is started) | `http://localhost:9200` |
+| `PW_OPENSEARCH_URL` | OpenSearch URL (only used when `opensearch` is started) | `http://localhost:9201` |
+| `PW_MINIO_URL` | Minio URL (only used when `minio` is started) | `http://localhost:9000` |
+| `PW_AZURITE_URL` | Azurite URL (only used when `azurite` is started) | `http://localhost:10000` |
+
+In `testcontainers` mode, these host/port defaults are never actually used — Testcontainers always assigns its own dynamic port or network alias per run, so a `testcontainers`-mode run can never collide with a same-machine `external`-mode session using the fixed defaults above. Tear a reused stack down explicitly with `npm run testcontainers:down` (from the `playwright/` package).
+
## Accessibility Testing
The library includes built-in accessibility testing using [axe-core](https://github.com/dequelabs/axe-core):
diff --git a/e2e-tests/playwright/lib/package.json b/e2e-tests/playwright/lib/package.json
index 79bc52652589..e0b0e284a312 100644
--- a/e2e-tests/playwright/lib/package.json
+++ b/e2e-tests/playwright/lib/package.json
@@ -44,17 +44,22 @@
},
"dependencies": {
"@axe-core/playwright": "4.12.1",
+ "@azure/storage-blob": "12.33.0",
"@mattermost/client": "file:../../../webapp/platform/client",
"@mattermost/types": "file:../../../webapp/platform/types",
"@percy/cli": "1.32.2",
"@percy/playwright": "1.1.0",
+ "@testcontainers/postgresql": "12.0.4",
"async-wait-until": "2.0.31",
"axe-core": "4.12.1",
"chalk": "5.6.2",
"deepmerge": "4.3.1",
"dotenv": "17.4.2",
+ "ldapts": "9.0.0",
"luxon": "3.7.2",
"mime-types": "3.0.2",
+ "minio": "8.0.7",
+ "testcontainers": "12.0.4",
"uuid": "14.0.1"
},
"devDependencies": {
diff --git a/e2e-tests/playwright/lib/rollup.config.js b/e2e-tests/playwright/lib/rollup.config.js
index 9423363f9a55..cf23e9d9f52f 100644
--- a/e2e-tests/playwright/lib/rollup.config.js
+++ b/e2e-tests/playwright/lib/rollup.config.js
@@ -18,7 +18,10 @@ export default {
plugins: [
typescript(),
copy({
- targets: [{src: 'src/asset/**/*', dest: 'dist/asset'}], // Copy assets to dist/
+ targets: [
+ {src: 'src/asset/**/*', dest: 'dist/asset'}, // Copy assets to dist/
+ {src: 'src/containers/assets/**/*', dest: 'dist/containers/assets'},
+ ],
}),
],
external: [
@@ -26,15 +29,22 @@ export default {
'@mattermost/client',
'@mattermost/types/config',
'@axe-core/playwright',
+ '@azure/storage-blob',
'@percy/playwright',
+ '@testcontainers/postgresql',
'dotenv',
+ 'ldapts',
'luxon',
+ 'minio',
'node:child_process',
'node:path',
'node:fs',
'node:fs/promises',
'node:os',
+ 'node:url',
+ 'node:util',
'mime-types',
+ 'testcontainers',
'uuid',
'async-wait-until',
'chalk',
diff --git a/e2e-tests/playwright/lib/src/containers/assets/Dockerfile.elasticsearch b/e2e-tests/playwright/lib/src/containers/assets/Dockerfile.elasticsearch
new file mode 100644
index 000000000000..1e3ac8d473e3
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/assets/Dockerfile.elasticsearch
@@ -0,0 +1,5 @@
+# Copied from server/build/Dockerfile.elasticsearch on 2026-07-19.
+ARG ELASTICSEARCH_VERSION=9.0.0
+FROM docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION}
+
+RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch analysis-icu analysis-nori analysis-kuromoji analysis-smartcn
diff --git a/e2e-tests/playwright/lib/src/containers/assets/Dockerfile.opensearch b/e2e-tests/playwright/lib/src/containers/assets/Dockerfile.opensearch
new file mode 100644
index 000000000000..c201ac19e7dc
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/assets/Dockerfile.opensearch
@@ -0,0 +1,5 @@
+# Copied from server/build/Dockerfile.opensearch on 2026-07-19.
+ARG OPENSEARCH_VERSION=3.0.0
+FROM opensearchproject/opensearch:$OPENSEARCH_VERSION
+
+RUN /usr/share/opensearch/bin/opensearch-plugin install analysis-icu analysis-nori analysis-kuromoji analysis-smartcn
diff --git a/e2e-tests/playwright/lib/src/containers/assets/README.md b/e2e-tests/playwright/lib/src/containers/assets/README.md
new file mode 100644
index 000000000000..e96539f15730
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/assets/README.md
@@ -0,0 +1,13 @@
+# Vendored assets
+
+These files are copied from `server/build/` in the Mattermost monorepo so that
+`@mattermost/playwright-lib`'s Testcontainers support is self-contained once published to npm
+(it can't reach outside its own package at runtime). If the source files change, these copies
+need to be refreshed manually — they are not symlinked or build-generated.
+
+| File | Copied from | Date |
+| ---------------------------- | ------------------------------------------------ | ---------- |
+| `postgres.conf` | `server/build/docker/postgres.conf` | 2026-07-19 |
+| `keycloak-realm-export.json` | `server/build/docker/keycloak/realm-export.json` | 2026-07-19 |
+| `Dockerfile.elasticsearch` | `server/build/Dockerfile.elasticsearch` | 2026-07-19 |
+| `Dockerfile.opensearch` | `server/build/Dockerfile.opensearch` | 2026-07-19 |
diff --git a/e2e-tests/playwright/lib/src/containers/assets/keycloak-realm-export.json b/e2e-tests/playwright/lib/src/containers/assets/keycloak-realm-export.json
new file mode 100644
index 000000000000..d01599566067
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/assets/keycloak-realm-export.json
@@ -0,0 +1,2333 @@
+{
+ "id": "b452bc2b-8c3d-4132-b096-8cb983bfa0fa",
+ "realm": "mattermost",
+ "notBefore": 0,
+ "defaultSignatureAlgorithm": "RS256",
+ "revokeRefreshToken": false,
+ "refreshTokenMaxReuse": 0,
+ "accessTokenLifespan": 300,
+ "accessTokenLifespanForImplicitFlow": 900,
+ "ssoSessionIdleTimeout": 1800,
+ "ssoSessionMaxLifespan": 36000,
+ "ssoSessionIdleTimeoutRememberMe": 0,
+ "ssoSessionMaxLifespanRememberMe": 0,
+ "offlineSessionIdleTimeout": 2592000,
+ "offlineSessionMaxLifespanEnabled": false,
+ "offlineSessionMaxLifespan": 5184000,
+ "clientSessionIdleTimeout": 0,
+ "clientSessionMaxLifespan": 0,
+ "clientOfflineSessionIdleTimeout": 0,
+ "clientOfflineSessionMaxLifespan": 0,
+ "accessCodeLifespan": 60,
+ "accessCodeLifespanUserAction": 300,
+ "accessCodeLifespanLogin": 1800,
+ "actionTokenGeneratedByAdminLifespan": 43200,
+ "actionTokenGeneratedByUserLifespan": 300,
+ "oauth2DeviceCodeLifespan": 600,
+ "oauth2DevicePollingInterval": 5,
+ "enabled": true,
+ "sslRequired": "none",
+ "registrationAllowed": false,
+ "registrationEmailAsUsername": false,
+ "rememberMe": false,
+ "verifyEmail": false,
+ "loginWithEmailAllowed": true,
+ "duplicateEmailsAllowed": false,
+ "resetPasswordAllowed": false,
+ "editUsernameAllowed": false,
+ "bruteForceProtected": false,
+ "permanentLockout": false,
+ "maxFailureWaitSeconds": 900,
+ "minimumQuickLoginWaitSeconds": 60,
+ "waitIncrementSeconds": 60,
+ "quickLoginCheckMilliSeconds": 1000,
+ "maxDeltaTimeSeconds": 43200,
+ "failureFactor": 30,
+ "roles": {
+ "realm": [
+ {
+ "id": "9406d8de-208f-411e-beb2-56c399f04d7a",
+ "name": "offline_access",
+ "description": "${role_offline-access}",
+ "composite": false,
+ "clientRole": false,
+ "containerId": "b452bc2b-8c3d-4132-b096-8cb983bfa0fa",
+ "attributes": {}
+ },
+ {
+ "id": "c4da0641-a06f-42af-87d7-4e8c060696c5",
+ "name": "uma_authorization",
+ "description": "${role_uma_authorization}",
+ "composite": false,
+ "clientRole": false,
+ "containerId": "b452bc2b-8c3d-4132-b096-8cb983bfa0fa",
+ "attributes": {}
+ },
+ {
+ "id": "44215b4f-7373-48b2-9882-ff6ea744c2b9",
+ "name": "default-roles-mattermost",
+ "description": "${role_default-roles}",
+ "composite": true,
+ "composites": {
+ "realm": ["offline_access", "uma_authorization"],
+ "client": {
+ "account": ["view-profile", "manage-account"]
+ }
+ },
+ "clientRole": false,
+ "containerId": "b452bc2b-8c3d-4132-b096-8cb983bfa0fa",
+ "attributes": {}
+ }
+ ],
+ "client": {
+ "realm-management": [
+ {
+ "id": "d4ababf1-bcef-4402-bb0a-95ae2ec3df27",
+ "name": "realm-admin",
+ "description": "${role_realm-admin}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "realm-management": [
+ "manage-identity-providers",
+ "query-clients",
+ "view-authorization",
+ "view-events",
+ "manage-users",
+ "view-clients",
+ "impersonation",
+ "query-realms",
+ "view-realm",
+ "view-identity-providers",
+ "query-users",
+ "create-client",
+ "manage-events",
+ "manage-realm",
+ "query-groups",
+ "manage-clients",
+ "view-users",
+ "manage-authorization"
+ ]
+ }
+ },
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "d91eb27c-53bf-48e2-b422-076358db1299",
+ "name": "manage-identity-providers",
+ "description": "${role_manage-identity-providers}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "ed38a7ec-8bb6-4175-abe5-5c5bf7a4e0e3",
+ "name": "query-clients",
+ "description": "${role_query-clients}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "a7eabbee-680e-4537-9e28-e85609384610",
+ "name": "manage-users",
+ "description": "${role_manage-users}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "49d8b34f-dddb-4120-b324-05689b84a12b",
+ "name": "view-authorization",
+ "description": "${role_view-authorization}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "73becff0-bcfd-4531-a3b3-9d655672de7d",
+ "name": "view-events",
+ "description": "${role_view-events}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "efb0b52e-d184-4d1f-b48b-ac295ee7dec0",
+ "name": "impersonation",
+ "description": "${role_impersonation}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "c00a62b0-03ab-470e-89cb-73d4d33ac8b9",
+ "name": "view-clients",
+ "description": "${role_view-clients}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "realm-management": ["query-clients"]
+ }
+ },
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "b13937d9-a445-4086-b6dd-349f06c57268",
+ "name": "query-realms",
+ "description": "${role_query-realms}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "9f2b221c-4615-4f45-a351-431518f3f694",
+ "name": "view-realm",
+ "description": "${role_view-realm}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "da6d79b8-fb13-47b1-9239-caac1a67674a",
+ "name": "query-users",
+ "description": "${role_query-users}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "ca7434e0-6d97-4ac6-8909-0a28027afbd6",
+ "name": "view-identity-providers",
+ "description": "${role_view-identity-providers}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "51218c0c-e7ea-4cb4-8504-b398db249d36",
+ "name": "create-client",
+ "description": "${role_create-client}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "cb1d1095-3d65-411d-971a-e1d769731bd4",
+ "name": "manage-events",
+ "description": "${role_manage-events}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "60b511a4-632e-4cce-8016-c8897cf532c3",
+ "name": "manage-realm",
+ "description": "${role_manage-realm}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "887e15d5-7fa2-41b0-8b9a-1f11f639c69a",
+ "name": "manage-clients",
+ "description": "${role_manage-clients}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "539f7011-2861-4155-ac00-3a41f743df4a",
+ "name": "query-groups",
+ "description": "${role_query-groups}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "9e2331cf-3b5e-49be-8ea9-be8c3fcf44ae",
+ "name": "manage-authorization",
+ "description": "${role_manage-authorization}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ },
+ {
+ "id": "66cc6fde-bfe9-41f0-8d72-fc679b8fed61",
+ "name": "view-users",
+ "description": "${role_view-users}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "realm-management": ["query-users", "query-groups"]
+ }
+ },
+ "clientRole": true,
+ "containerId": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "attributes": {}
+ }
+ ],
+ "mattermost": [],
+ "mattermost-openid": [
+ {
+ "id": "4ee3d3fb-4a1d-4933-b45f-cde999835106",
+ "name": "uma_protection",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "427658c0-080b-426b-b37b-d69e4ced5890",
+ "attributes": {}
+ }
+ ],
+ "security-admin-console": [],
+ "admin-cli": [],
+ "account-console": [],
+ "broker": [
+ {
+ "id": "c09bb3a4-54c6-4adb-9a31-b175df4a9bc8",
+ "name": "read-token",
+ "description": "${role_read-token}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "b4b6df43-11bd-47a8-9ba2-6d974b4a5ac1",
+ "attributes": {}
+ }
+ ],
+ "account": [
+ {
+ "id": "0d75120c-b321-4122-ac81-2d15ef0b4635",
+ "name": "view-consent",
+ "description": "${role_view-consent}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "aad4a0e3-b4ba-4530-975e-a7ccf9cdb53e",
+ "attributes": {}
+ },
+ {
+ "id": "46dd8b66-9a4c-4bac-af6f-ee722902db7d",
+ "name": "view-groups",
+ "description": "${role_view-groups}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "aad4a0e3-b4ba-4530-975e-a7ccf9cdb53e",
+ "attributes": {}
+ },
+ {
+ "id": "809817c3-1da5-46ec-8ee9-e6a08a772494",
+ "name": "manage-account-links",
+ "description": "${role_manage-account-links}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "aad4a0e3-b4ba-4530-975e-a7ccf9cdb53e",
+ "attributes": {}
+ },
+ {
+ "id": "c93de053-4e96-44f2-9984-1ccf092fd22e",
+ "name": "view-profile",
+ "description": "${role_view-profile}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "aad4a0e3-b4ba-4530-975e-a7ccf9cdb53e",
+ "attributes": {}
+ },
+ {
+ "id": "48bc7f84-3407-4e0f-aaec-be5b7010b5b9",
+ "name": "delete-account",
+ "description": "${role_delete-account}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "aad4a0e3-b4ba-4530-975e-a7ccf9cdb53e",
+ "attributes": {}
+ },
+ {
+ "id": "6a3d352b-61cd-49d3-833a-7dfb5e40b3ac",
+ "name": "manage-account",
+ "description": "${role_manage-account}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "account": ["manage-account-links"]
+ }
+ },
+ "clientRole": true,
+ "containerId": "aad4a0e3-b4ba-4530-975e-a7ccf9cdb53e",
+ "attributes": {}
+ },
+ {
+ "id": "70ff6d75-0c05-4d75-a477-05a31aa0508d",
+ "name": "manage-consent",
+ "description": "${role_manage-consent}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "account": ["view-consent"]
+ }
+ },
+ "clientRole": true,
+ "containerId": "aad4a0e3-b4ba-4530-975e-a7ccf9cdb53e",
+ "attributes": {}
+ },
+ {
+ "id": "99ce72ff-29dc-4e38-b384-8618e0e55887",
+ "name": "view-applications",
+ "description": "${role_view-applications}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "aad4a0e3-b4ba-4530-975e-a7ccf9cdb53e",
+ "attributes": {}
+ }
+ ]
+ }
+ },
+ "groups": [
+ {
+ "id": "08c66283-6438-4c88-b373-a5d97fd98a31",
+ "name": "simpsons",
+ "path": "/simpsons",
+ "subGroups": [],
+ "attributes": {},
+ "realmRoles": [],
+ "clientRoles": {}
+ }
+ ],
+ "defaultRole": {
+ "id": "44215b4f-7373-48b2-9882-ff6ea744c2b9",
+ "name": "default-roles-mattermost",
+ "description": "${role_default-roles}",
+ "composite": true,
+ "clientRole": false,
+ "containerId": "b452bc2b-8c3d-4132-b096-8cb983bfa0fa"
+ },
+ "requiredCredentials": ["password"],
+ "otpPolicyType": "totp",
+ "otpPolicyAlgorithm": "HmacSHA1",
+ "otpPolicyInitialCounter": 0,
+ "otpPolicyDigits": 6,
+ "otpPolicyLookAheadWindow": 1,
+ "otpPolicyPeriod": 30,
+ "otpPolicyCodeReusable": false,
+ "otpSupportedApplications": ["totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName"],
+ "localizationTexts": {},
+ "webAuthnPolicyRpEntityName": "keycloak",
+ "webAuthnPolicySignatureAlgorithms": ["ES256"],
+ "webAuthnPolicyRpId": "",
+ "webAuthnPolicyAttestationConveyancePreference": "not specified",
+ "webAuthnPolicyAuthenticatorAttachment": "not specified",
+ "webAuthnPolicyRequireResidentKey": "not specified",
+ "webAuthnPolicyUserVerificationRequirement": "not specified",
+ "webAuthnPolicyCreateTimeout": 0,
+ "webAuthnPolicyAvoidSameAuthenticatorRegister": false,
+ "webAuthnPolicyAcceptableAaguids": [],
+ "webAuthnPolicyExtraOrigins": [],
+ "webAuthnPolicyPasswordlessRpEntityName": "keycloak",
+ "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"],
+ "webAuthnPolicyPasswordlessRpId": "",
+ "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
+ "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
+ "webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
+ "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
+ "webAuthnPolicyPasswordlessCreateTimeout": 0,
+ "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
+ "webAuthnPolicyPasswordlessAcceptableAaguids": [],
+ "webAuthnPolicyPasswordlessExtraOrigins": [],
+ "users": [
+ {
+ "id": "d3aa00db-7722-4d45-94f5-beb3ad8d6794",
+ "username": "homer",
+ "enabled": true,
+ "totp": false,
+ "emailVerified": true,
+ "firstName": "Homer",
+ "lastName": "Simpson",
+ "email": "homer.simpson@sample.com",
+ "credentials": [
+ {
+ "id": "bc3b2b63-38b2-420a-8802-f33e573eda81",
+ "type": "password",
+ "createdDate": 1714991408927,
+ "secretData": "{\"value\":\"iCgLsWLJO27kDrtXvpM9y7eZJbcNMZpNq+rMVr0gxoQ=\",\"salt\":\"JWIwdHDhfmb0ZxK3B2VrjA==\",\"additionalParameters\":{}}",
+ "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
+ }
+ ],
+ "disableableCredentialTypes": [],
+ "requiredActions": [],
+ "notBefore": 0,
+ "groups": []
+ },
+ {
+ "id": "dfa4ab33-bf3f-424d-9d37-ee222249729a",
+ "username": "lisa",
+ "enabled": true,
+ "totp": false,
+ "emailVerified": true,
+ "firstName": "Lisa",
+ "lastName": "Simpson",
+ "email": "lisa.simpson@sample.com",
+ "credentials": [
+ {
+ "id": "00a5098e-23df-49c7-98e1-73a5fe95195a",
+ "type": "password",
+ "createdDate": 1714991408973,
+ "secretData": "{\"value\":\"+PFQnePwCy0hhd3IsueLtv2BGskX2egPLBTUeOoOBK0=\",\"salt\":\"eOBehbwDrie/0eUwpBrlAg==\",\"additionalParameters\":{}}",
+ "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
+ }
+ ],
+ "disableableCredentialTypes": [],
+ "requiredActions": [],
+ "notBefore": 0,
+ "groups": []
+ },
+ {
+ "id": "337ba922-6496-4517-a4e6-f9f594c237b6",
+ "username": "marge",
+ "enabled": true,
+ "totp": false,
+ "emailVerified": true,
+ "firstName": "Marge",
+ "lastName": "Simpson",
+ "email": "marge.simpson@sample.com",
+ "credentials": [
+ {
+ "id": "768c667c-5480-4176-b272-0fa6b30aff06",
+ "type": "password",
+ "createdDate": 1714991408951,
+ "secretData": "{\"value\":\"u2ZcUnBkjiFSUFxP3wyvBRelU+k4ZSMQhy285S2cvI0=\",\"salt\":\"PXIfRXN3zZ3wIltsfqlRtQ==\",\"additionalParameters\":{}}",
+ "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
+ }
+ ],
+ "disableableCredentialTypes": [],
+ "requiredActions": [],
+ "notBefore": 0,
+ "groups": []
+ },
+ {
+ "id": "bf6d32c1-6364-4ac0-8dca-10d69ebfefa1",
+ "createdTimestamp": 1714991464428,
+ "username": "service-account-mattermost-openid",
+ "enabled": true,
+ "totp": false,
+ "emailVerified": false,
+ "serviceAccountClientId": "mattermost-openid",
+ "credentials": [],
+ "disableableCredentialTypes": [],
+ "requiredActions": [],
+ "realmRoles": ["default-roles-mattermost"],
+ "clientRoles": {
+ "mattermost-openid": ["uma_protection"]
+ },
+ "notBefore": 0,
+ "groups": []
+ }
+ ],
+ "scopeMappings": [
+ {
+ "clientScope": "offline_access",
+ "roles": ["offline_access"]
+ }
+ ],
+ "clientScopeMappings": {
+ "account": [
+ {
+ "client": "account-console",
+ "roles": ["manage-account", "view-groups"]
+ }
+ ]
+ },
+ "clients": [
+ {
+ "id": "aad4a0e3-b4ba-4530-975e-a7ccf9cdb53e",
+ "clientId": "account",
+ "name": "${client_account}",
+ "rootUrl": "${authBaseUrl}",
+ "baseUrl": "/realms/mattermost/account/",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": ["/realms/mattermost/account/*"],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"],
+ "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
+ },
+ {
+ "id": "7daae697-1f13-4928-ad46-f4ef214cc9d4",
+ "clientId": "account-console",
+ "name": "${client_account-console}",
+ "rootUrl": "${authBaseUrl}",
+ "baseUrl": "/realms/mattermost/account/",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": ["/realms/mattermost/account/*"],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+",
+ "pkce.code.challenge.method": "S256"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "protocolMappers": [
+ {
+ "id": "e1413711-1419-484d-b41e-c144b902de8b",
+ "name": "audience resolve",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-audience-resolve-mapper",
+ "consentRequired": false,
+ "config": {}
+ }
+ ],
+ "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"],
+ "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
+ },
+ {
+ "id": "c24ac0ab-f5ef-4185-b23d-9eca4d84edeb",
+ "clientId": "admin-cli",
+ "name": "${client_admin-cli}",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": [],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": false,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"],
+ "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
+ },
+ {
+ "id": "b4b6df43-11bd-47a8-9ba2-6d974b4a5ac1",
+ "clientId": "broker",
+ "name": "${client_broker}",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": [],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": true,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": false,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"],
+ "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
+ },
+ {
+ "id": "88e48acd-0b87-4e5b-a1e0-94ba87c9471c",
+ "clientId": "mattermost",
+ "name": "",
+ "description": "",
+ "rootUrl": "",
+ "adminUrl": "",
+ "baseUrl": "",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": ["*"],
+ "webOrigins": ["*"],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": true,
+ "protocol": "saml",
+ "attributes": {
+ "saml.assertion.signature": "true",
+ "saml.force.post.binding": "true",
+ "saml.encrypt": "false",
+ "post.logout.redirect.uris": "+",
+ "saml.server.signature": "true",
+ "saml.server.signature.keyinfo.ext": "false",
+ "saml.signing.certificate": "MIICozCCAYsCBgGNzPeaJzANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDDAptYXR0ZXJtb3N0MB4XDTI0MDIyMTE4MzgxNVoXDTM0MDIyMTE4Mzk1NVowFTETMBEGA1UEAwwKbWF0dGVybW9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALTGWjhcVlJ9u+/b/Q4laMj1mEYEMuNmP+ZB9zTICk7sQbihJ1JRY8vJ450GXcFRTEuSWLOavSsInLZNkYqtBQ1/cVBmj8NgqMBOyLXsa1BnSE+mB3S3MpmWZwMIY1/1fPpEW8potkIKRigidkoa1zi8U/0fDu6sA9wzygUdeckX3TxN2sIwAr7beBdcwEKyKZS5VGATj3HU7TUAIyk02Xbq5vgKIR9KqdazHIuV46I0BPgQ+WbXROviq/U7/txxXu6qxEuvAxwXxFa56fYQW6qpWS3s4muEGFyi9uKbeUeKLPYSjiZYFHOloAZ3ZILbC3k8dpPWSUq5lH5jsuPkEBkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAO/cKwQAJBNSdwl/s4evpcfNAZMnvRi3IvhLKDbv648/eQQEhBVK7FQ8J9iQJaVbhdkZCSOF+x+HUURX/82h+CKNj0SehxPaELpVpiLXWh+KKBAiRHWTvsmjgfuqa5dWcmjBc2W1r+kOtvG/KZnxdmSR9fZc/BUF+64FUY+m7SMUBNDndn28Bc4wXZVWjHUF6NjTzplm5Karvcd7E892PwApSytwPA7BiPdL6DKgOOPya5Klyyykwz2jn74LWi+fEIWb7coaamwZ+bSU1eQINp3mjJz3w+EKX6gmbsSWopSm8cMDhh8l3/Dc1mh1gpiZV+zxoWpPFvRnDWJtV8iBsqw==",
+ "saml.artifact.binding.identifier": "s+FCT7acoISBsDrZ2B6VSIQhmX8=",
+ "saml.artifact.binding": "false",
+ "saml.signature.algorithm": "RSA_SHA256",
+ "saml_force_name_id_format": "true",
+ "saml.client.signature": "false",
+ "saml.authnstatement": "true",
+ "display.on.consent.screen": "false",
+ "saml_name_id_format": "email",
+ "saml.signing.private.key": "MIIEowIBAAKCAQEAtMZaOFxWUn2779v9DiVoyPWYRgQy42Y/5kH3NMgKTuxBuKEnUlFjy8njnQZdwVFMS5JYs5q9Kwictk2Riq0FDX9xUGaPw2CowE7ItexrUGdIT6YHdLcymZZnAwhjX/V8+kRbymi2QgpGKCJ2ShrXOLxT/R8O7qwD3DPKBR15yRfdPE3awjACvtt4F1zAQrIplLlUYBOPcdTtNQAjKTTZdurm+AohH0qp1rMci5XjojQE+BD5ZtdE6+Kr9Tv+3HFe7qrES68DHBfEVrnp9hBbqqlZLezia4QYXKL24pt5R4os9hKOJlgUc6WgBndkgtsLeTx2k9ZJSrmUfmOy4+QQGQIDAQABAoIBABCFsgSFdPrOZN1R+hWfR+3ZG5EnrFaCF4xGIm/bqiyHNaUILqG8kF5VfhURmkNTWMGt9yc4OPqsS3Vyjr8x29y9VYgbjZ9178p+0OOmv3Kuc+qaskMBxPFKcLETZNDGDUWK9qg/7IuJ6JpdtvnELQBRnywK+QiJpoMrZRcVV/aP7VMXuJJGZ+wW9LaCQBy+6powmpS/PnNO9DsN8UdBEGv5L08zF8yYH6gzv4bfubTwfDhL2JHTgikw8vA6ED1kpJjS8hFsBHB7WyN4AhDm4oLW83TJqdojcI7A0R/4atovl2wR/Yg9NLjIruOa7c60oziDN0rG2x9Wv2XFHl0KhQECgYEA4L9MdN3AQayYVH+Ftvz3n9Gkd8zt/NkCOuuwxMOQqHSyzpRohvreZ6HmIIw1P8vV5ll+obfKyxIM5Ry/KAO9wtJxF+6akvZL0yf1eWewaBI2C4uB2zviGfaKw0bTl+uRmOnK2R3yX8i9XSqhUuIwmXqimwQJ6QTeqopniVj9fxkCgYEAzemxj29CKNSjXPixZ6aFJdmo/k1UaqZljPLFyf6E9/TEk4ZKjy1XsiQoc5zlhEtg1QF/mhkMW+djpeA3y9D/Ytd5CGbifjgsIZz8AmEgkXv/qSu0uwCANKh74mR9st2genY+deR4csfmH0btD4suZM2epbzkYjATGDeQpauqOQECgYByZEv5C8lIZB3gnEul2Nn8yYrJw0+6q4W8EayWaefeJg/An78YPOYonznvhQ2xXAGSTijrDQ42EAjqMJiHOGhEmjIf+T1J4fy0DBmgYotiXSqO88HzoWYyys7UF9+o6qjfyNeLbPepkxX3LrSP6DrOU6Ih/otAxE7FRM7wFJBqOQKBgH/PiaY1cgR0ED66IyvqIYVf+FR43WghhrEZVAnQUOqz5HyJg6ghheiOJwm0HoaiXyV3+6ND3vcAxkqExT1eqdbJVlgPvJCJ/2KR1Z8IeLW1GPkBpvnYErwcxGC+7YLjrCO3V4fH27erOry5QpY+Vvq84rePatdDbea/PqqGxm8BAoGBAK8DpUktGogqRi5EwxyvWBwWiJCuQIzn5TW6YHTFLE6VA69KOBvSUoTpMqeOfxARAVwgwDb0kF/RX7vnoMROwn8K7dma3DnCS9+TN9a9GsI9jezU+TbdUeAcORAjRSxFZezKkqQcwU8beP4ofEwBv4n6TjbnNo5RsGVFBHeNErk6",
+ "saml.allow.ecp.flow": "false",
+ "saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#",
+ "saml.onetimeuse.condition": "false",
+ "saml.server.signature.keyinfo.xmlSigKeyInfoKeyNameTransformer": "NONE"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": true,
+ "nodeReRegistrationTimeout": -1,
+ "protocolMappers": [
+ {
+ "id": "1a6ae5ee-4561-42dc-b638-d0fd69ac06ba",
+ "name": "X500 givenName",
+ "protocol": "saml",
+ "protocolMapper": "saml-user-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ "user.attribute": "firstName",
+ "friendly.name": "givenName",
+ "attribute.name": "urn:oid:2.5.4.42"
+ }
+ },
+ {
+ "id": "c1f536b0-176d-411d-bbb6-38544c36570b",
+ "name": "Username",
+ "protocol": "saml",
+ "protocolMapper": "saml-user-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "username",
+ "attribute.nameformat": "Basic",
+ "attribute.name": "username"
+ }
+ },
+ {
+ "id": "2b955e9f-139c-4280-880f-afb063b48b5b",
+ "name": "ID",
+ "protocol": "saml",
+ "protocolMapper": "saml-user-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "id",
+ "attribute.nameformat": "Basic",
+ "attribute.name": "id"
+ }
+ },
+ {
+ "id": "3cadb4fe-4ca8-4a29-b8b4-5089ee5c7fe7",
+ "name": "X500 email",
+ "protocol": "saml",
+ "protocolMapper": "saml-user-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ "user.attribute": "email",
+ "friendly.name": "email",
+ "attribute.name": "urn:oid:1.2.840.113549.1.9.1"
+ }
+ },
+ {
+ "id": "fb260c06-9fbb-4f8e-938d-ce9c21b5c999",
+ "name": "X500 surname",
+ "protocol": "saml",
+ "protocolMapper": "saml-user-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "attribute.nameformat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ "user.attribute": "lastName",
+ "friendly.name": "surname",
+ "attribute.name": "urn:oid:2.5.4.4"
+ }
+ }
+ ],
+ "defaultClientScopes": ["role_list"],
+ "optionalClientScopes": []
+ },
+ {
+ "id": "427658c0-080b-426b-b37b-d69e4ced5890",
+ "clientId": "mattermost-openid",
+ "name": "",
+ "description": "",
+ "rootUrl": "",
+ "adminUrl": "",
+ "baseUrl": "",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "9Y7dykcoA9luTC77XtXxOu9UbNx3rhj6",
+ "redirectUris": ["*"],
+ "webOrigins": ["*"],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": true,
+ "publicClient": false,
+ "frontchannelLogout": true,
+ "protocol": "openid-connect",
+ "attributes": {
+ "oidc.ciba.grant.enabled": "false",
+ "client.secret.creation.time": "1714991470",
+ "backchannel.logout.session.required": "true",
+ "post.logout.redirect.uris": "*",
+ "oauth2.device.authorization.grant.enabled": "false",
+ "display.on.consent.screen": "false",
+ "backchannel.logout.revoke.offline.tokens": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": true,
+ "nodeReRegistrationTimeout": -1,
+ "protocolMappers": [
+ {
+ "id": "3741bfe0-ad4f-49f6-bb44-4c05e8f8e958",
+ "name": "Client IP Address",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientAddress",
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientAddress",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "52456e14-87ba-431e-9ff4-420d196fb838",
+ "name": "Client ID",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "client_id",
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "client_id",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "a7f2c348-149e-4785-9491-37c6561486d4",
+ "name": "Client Host",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "clientHost",
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "clientHost",
+ "jsonType.label": "String"
+ }
+ }
+ ],
+ "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"],
+ "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
+ },
+ {
+ "id": "d7c4c70f-6d4b-469f-afec-366c4494e9b4",
+ "clientId": "realm-management",
+ "name": "${client_realm-management}",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": [],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": true,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": false,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"],
+ "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
+ },
+ {
+ "id": "7a8da89a-19a7-4821-86cb-e676bd9c1997",
+ "clientId": "security-admin-console",
+ "name": "${client_security-admin-console}",
+ "rootUrl": "${authAdminUrl}",
+ "baseUrl": "/admin/mattermost/console/",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": ["/admin/mattermost/console/*"],
+ "webOrigins": ["+"],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+",
+ "pkce.code.challenge.method": "S256"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "protocolMappers": [
+ {
+ "id": "da60c790-d3ba-420f-b87c-17d5205891f8",
+ "name": "locale",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "locale",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "locale",
+ "jsonType.label": "String"
+ }
+ }
+ ],
+ "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"],
+ "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
+ }
+ ],
+ "clientScopes": [
+ {
+ "id": "deaaf9e4-f944-48f0-844d-69acefa4fdee",
+ "name": "offline_access",
+ "description": "OpenID Connect built-in scope: offline_access",
+ "protocol": "openid-connect",
+ "attributes": {
+ "consent.screen.text": "${offlineAccessScopeConsentText}",
+ "display.on.consent.screen": "true"
+ }
+ },
+ {
+ "id": "9e05629c-59d0-49da-994c-a27b3d1ea181",
+ "name": "roles",
+ "description": "OpenID Connect scope for add user roles to the access token",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "true",
+ "consent.screen.text": "${rolesScopeConsentText}"
+ },
+ "protocolMappers": [
+ {
+ "id": "30b9b185-a7a2-40ea-8400-10aa862fcaa0",
+ "name": "audience resolve",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-audience-resolve-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "access.token.claim": "true"
+ }
+ },
+ {
+ "id": "c6282965-cd18-49ad-92eb-9c1aac384e88",
+ "name": "client roles",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-client-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "multivalued": "true",
+ "user.attribute": "foo",
+ "access.token.claim": "true",
+ "claim.name": "resource_access.${client_id}.roles",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "cb453a01-226a-44c4-8490-e72d24369c9b",
+ "name": "realm roles",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-realm-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "multivalued": "true",
+ "user.attribute": "foo",
+ "access.token.claim": "true",
+ "claim.name": "realm_access.roles",
+ "jsonType.label": "String"
+ }
+ }
+ ]
+ },
+ {
+ "id": "0916ac9b-4712-4ffa-a7ea-79cf38afc52f",
+ "name": "microprofile-jwt",
+ "description": "Microprofile - JWT built-in scope",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "id": "f05b04c6-d8b2-4865-a579-b39c25d112f1",
+ "name": "groups",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-realm-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "multivalued": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "foo",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "groups",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "94a133ec-e757-456f-bca3-8cfd7151d18f",
+ "name": "upn",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "username",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "upn",
+ "jsonType.label": "String"
+ }
+ }
+ ]
+ },
+ {
+ "id": "29ac3b36-ce41-4050-9d63-945346650aab",
+ "name": "web-origins",
+ "description": "OpenID Connect scope for add allowed web origins to the access token",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "false",
+ "consent.screen.text": ""
+ },
+ "protocolMappers": [
+ {
+ "id": "5307d33c-19ac-4b4c-ae1e-f048b3a58458",
+ "name": "allowed web origins",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-allowed-origins-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "access.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "id": "72fe0a5e-bed9-493a-8b11-b518be450989",
+ "name": "acr",
+ "description": "OpenID Connect scope for add acr (authentication context class reference) to the token",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "id": "00548232-102a-45e6-9c91-16dedc2a2db8",
+ "name": "acr loa level",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-acr-mapper",
+ "consentRequired": false,
+ "config": {
+ "id.token.claim": "true",
+ "introspection.token.claim": "true",
+ "access.token.claim": "true",
+ "userinfo.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "id": "f7d7585c-2e71-40c9-b8ab-aa022d1e2ad6",
+ "name": "role_list",
+ "description": "SAML role list",
+ "protocol": "saml",
+ "attributes": {
+ "consent.screen.text": "${samlRoleListScopeConsentText}",
+ "display.on.consent.screen": "true"
+ },
+ "protocolMappers": [
+ {
+ "id": "bf40cc9f-a059-419b-a795-b6400d8fd3c9",
+ "name": "role list",
+ "protocol": "saml",
+ "protocolMapper": "saml-role-list-mapper",
+ "consentRequired": false,
+ "config": {
+ "single": "false",
+ "attribute.nameformat": "Basic",
+ "attribute.name": "Role"
+ }
+ }
+ ]
+ },
+ {
+ "id": "5350312e-9032-4d75-b5ec-075e5b61b7c6",
+ "name": "address",
+ "description": "OpenID Connect built-in scope: address",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "true",
+ "consent.screen.text": "${addressScopeConsentText}"
+ },
+ "protocolMappers": [
+ {
+ "id": "1748a067-e54a-4bb2-9982-bf0ab8741873",
+ "name": "address",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-address-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute.formatted": "formatted",
+ "user.attribute.country": "country",
+ "introspection.token.claim": "true",
+ "user.attribute.postal_code": "postal_code",
+ "userinfo.token.claim": "true",
+ "user.attribute.street": "street",
+ "id.token.claim": "true",
+ "user.attribute.region": "region",
+ "access.token.claim": "true",
+ "user.attribute.locality": "locality"
+ }
+ }
+ ]
+ },
+ {
+ "id": "e56ed83f-f9ce-47cf-a022-a489b4835ec8",
+ "name": "profile",
+ "description": "OpenID Connect built-in scope: profile",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "true",
+ "consent.screen.text": "${profileScopeConsentText}"
+ },
+ "protocolMappers": [
+ {
+ "id": "779f68b0-7898-4f85-a060-be5ee1d9f8d7",
+ "name": "profile",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "profile",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "profile",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "83ee31c0-3937-47ab-b690-1e02d4d17e2e",
+ "name": "family name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "lastName",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "family_name",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "6390be9c-5111-492a-ae0c-26a2f9b0a37e",
+ "name": "nickname",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "nickname",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "nickname",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "746c53b2-d4a3-42ea-9862-17e8b51caa9c",
+ "name": "birthdate",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "birthdate",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "birthdate",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "caac0685-5ba9-4a4e-b0e6-a74d2e5a573f",
+ "name": "full name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-full-name-mapper",
+ "consentRequired": false,
+ "config": {
+ "id.token.claim": "true",
+ "introspection.token.claim": "true",
+ "access.token.claim": "true",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "b5c4a167-0973-4852-a946-67440d0f28c4",
+ "name": "website",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "website",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "website",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "fd9379e3-6faa-47b2-a7c7-24c782d185fa",
+ "name": "updated at",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "updatedAt",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "updated_at",
+ "jsonType.label": "long"
+ }
+ },
+ {
+ "id": "7fe61e51-18c6-48a9-a61a-e15d1ccbfb8e",
+ "name": "username",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "username",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "preferred_username",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "fa7b1ff8-71e7-4c3b-9316-eef920b2c1da",
+ "name": "picture",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "picture",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "picture",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "ad81c29f-7977-48fa-8800-7fd5dd55a210",
+ "name": "middle name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "middleName",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "middle_name",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "094011f2-703d-4421-92ab-3844363c810e",
+ "name": "gender",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "gender",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "gender",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "3b2d4d95-e77f-413a-8a34-8d6bd89ca07f",
+ "name": "locale",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "locale",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "locale",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "4dd32281-592b-4992-af19-2950da495776",
+ "name": "zoneinfo",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "zoneinfo",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "zoneinfo",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "bf5eff8d-a7a2-4e38-aa65-d84f6e164ce6",
+ "name": "given name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "firstName",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "given_name",
+ "jsonType.label": "String"
+ }
+ }
+ ]
+ },
+ {
+ "id": "10a8601c-f832-4de4-b067-c80b5b6ea41c",
+ "name": "email",
+ "description": "OpenID Connect built-in scope: email",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "true",
+ "consent.screen.text": "${emailScopeConsentText}"
+ },
+ "protocolMappers": [
+ {
+ "id": "1f74af90-79f2-4a84-bb24-fd476690b440",
+ "name": "email",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "email",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "email",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "374eca64-4920-45bd-84c1-7b4902f6efe2",
+ "name": "email verified",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "emailVerified",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "email_verified",
+ "jsonType.label": "boolean"
+ }
+ }
+ ]
+ },
+ {
+ "id": "a5b641cf-c886-4a2b-9336-149f1468927f",
+ "name": "phone",
+ "description": "OpenID Connect built-in scope: phone",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "true",
+ "consent.screen.text": "${phoneScopeConsentText}"
+ },
+ "protocolMappers": [
+ {
+ "id": "8c643186-80dc-4860-8aa3-96c40e49175c",
+ "name": "phone number verified",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "phoneNumberVerified",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "phone_number_verified",
+ "jsonType.label": "boolean"
+ }
+ },
+ {
+ "id": "4eb37f9e-d1e5-464e-acd5-8c6103f6843d",
+ "name": "phone number",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "phoneNumber",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "phone_number",
+ "jsonType.label": "String"
+ }
+ }
+ ]
+ }
+ ],
+ "defaultDefaultClientScopes": ["role_list", "profile", "email", "roles", "web-origins", "acr"],
+ "defaultOptionalClientScopes": ["offline_access", "address", "phone", "microprofile-jwt"],
+ "browserSecurityHeaders": {
+ "contentSecurityPolicyReportOnly": "",
+ "xContentTypeOptions": "nosniff",
+ "referrerPolicy": "no-referrer",
+ "xRobotsTag": "none",
+ "xFrameOptions": "SAMEORIGIN",
+ "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
+ "xXSSProtection": "1; mode=block",
+ "strictTransportSecurity": "max-age=31536000; includeSubDomains"
+ },
+ "smtpServer": {},
+ "eventsEnabled": false,
+ "eventsListeners": ["jboss-logging"],
+ "enabledEventTypes": [],
+ "adminEventsEnabled": false,
+ "adminEventsDetailsEnabled": false,
+ "identityProviders": [],
+ "identityProviderMappers": [],
+ "components": {
+ "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [
+ {
+ "id": "045ba5df-2445-401a-a8a7-b4083e5a2128",
+ "name": "Allowed Client Scopes",
+ "providerId": "allowed-client-templates",
+ "subType": "authenticated",
+ "subComponents": {},
+ "config": {
+ "allow-default-scopes": ["true"]
+ }
+ },
+ {
+ "id": "e95d38ab-da8f-4760-9346-73f5c3f25632",
+ "name": "Full Scope Disabled",
+ "providerId": "scope",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {}
+ },
+ {
+ "id": "fabadbed-3fca-4a77-84ca-f4a6973c91d2",
+ "name": "Trusted Hosts",
+ "providerId": "trusted-hosts",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "host-sending-registration-request-must-match": ["true"],
+ "client-uris-must-match": ["true"]
+ }
+ },
+ {
+ "id": "0fb88997-b088-4bf9-8f28-7775fefdda78",
+ "name": "Allowed Protocol Mapper Types",
+ "providerId": "allowed-protocol-mappers",
+ "subType": "authenticated",
+ "subComponents": {},
+ "config": {
+ "allowed-protocol-mapper-types": [
+ "saml-user-attribute-mapper",
+ "oidc-usermodel-property-mapper",
+ "oidc-usermodel-attribute-mapper",
+ "oidc-address-mapper",
+ "saml-user-property-mapper",
+ "oidc-sha256-pairwise-sub-mapper",
+ "oidc-full-name-mapper",
+ "saml-role-list-mapper"
+ ]
+ }
+ },
+ {
+ "id": "1eef8c1c-87c1-4a25-8c5a-57ca2c31b2d1",
+ "name": "Allowed Client Scopes",
+ "providerId": "allowed-client-templates",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "allow-default-scopes": ["true"]
+ }
+ },
+ {
+ "id": "a15ad9ca-5f30-41be-9f14-b9b12f34150a",
+ "name": "Max Clients Limit",
+ "providerId": "max-clients",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "max-clients": ["200"]
+ }
+ },
+ {
+ "id": "ee38000a-a362-430c-a836-e651e13e4b2f",
+ "name": "Allowed Protocol Mapper Types",
+ "providerId": "allowed-protocol-mappers",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "allowed-protocol-mapper-types": [
+ "saml-user-property-mapper",
+ "saml-role-list-mapper",
+ "oidc-usermodel-property-mapper",
+ "oidc-address-mapper",
+ "saml-user-attribute-mapper",
+ "oidc-usermodel-attribute-mapper",
+ "oidc-full-name-mapper",
+ "oidc-sha256-pairwise-sub-mapper"
+ ]
+ }
+ },
+ {
+ "id": "4bc26e75-f724-440f-aca9-11706ca534a8",
+ "name": "Consent Required",
+ "providerId": "consent-required",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {}
+ }
+ ],
+ "org.keycloak.keys.KeyProvider": [
+ {
+ "id": "b25fb4b0-a916-427a-9a0c-5e37257d754a",
+ "name": "rsa-generated",
+ "providerId": "rsa-generated",
+ "subComponents": {},
+ "config": {
+ "privateKey": [
+ "MIIEpAIBAAKCAQEAwJ/6a/8r+O2FIa4N9jhGRWazYb689YtBBVbJ+a50QlWv/SsP1aZl/0HUpKoGIhDaKwAedx7+30CsHKoD3p2Gh7OGsY4CQ3giOgHJ0vx95P5ZPVq4oe6P3KA85MK/CWEAOQFHqnzPQveSViYADuvY2TOnvvNiPuh9SgpmXKa977AiSe20kdUoiGbzdRzJuGPHD03+Ts8KVZqimNJGZJI+L4nszm/aQ4LM333UZackCezPYA9svRsva+NbAn7+Yh05o5b6Ozvn6hwuaZ07j4i3VXR/IOj8scAyZPKClMrJsWT8qu7KZcWvUNKoT0wpMwV+RJLxO2JDSFGVxQRbOCADDwIDAQABAoIBABEjCm3qPTi/tNzqtb5GgmXxPZtAE2KK+YVmk8B9V0WBhw8wcg+xZpiffDIMGFCWjDpQH+W8IbOt9sH0FDUNcacH/z8zoZrsCKt9dj2pje+9+yV8uTla1nyr2s8FTmagtRrdNMmDohLUoLW+Wrn5HIkyUcWotIBhQahBJZsvkG1O6C4JigYqQUirtitQf65BpLeaqKYcwnU/i/IfkvwHPYxxT+YmnUEK65OPu931cQqmvUgEuY2oluL/SLjjanwQPHUVKQSF8XCWW7kP3yD3CHjyuBuKR9vYZmIhy0hwKta3zvz98LuSW73kb5V43gJVcig1d0+mxmfFkxdjOrY2Z9UCgYEA5QyM+cyju4cCVgOteBIdwG20XX9T5QNPoxuf/xXDnBq3Z1epsEO15kJNcjqcnm1SsGSimjgSeHtwAZ5jIqob0a/FTsPVEMWfD8MBomwmLEvMik2vjMwMmJAbUQsr0P2T6ZPJEBbIcG0s0x6s2OFG+iLVmwwvA6RZ8OTG6Jd8C2MCgYEA10pBRHnK3Hd7MLTH5s/+MfTlr54Ad2fDZ5AvcRpUEDExQfr/4lBn9Azll0mFX7Gko8SE4VfpzVP69+jr6th8rJZKt5OiuzHB7i5SAGotjNEYa/o3/htM+uiRq6IDdmnODtF4VDliCt7Pae3Xg/00AwQvS+er/Y7v0AnIsc/x92UCgYEAhf1bkt3VgmQuR6cLGUxvvxvuASv1JelT8vS8GfrfXTh7xaaQi55wkGyE5afH6x3E249jV5gppfV/xlKjZ6nfcEDEyZjh0Vfp/sxy7lMZ2ZvV4aokYdcUH0Nd14ETdsSAfImJezH4u1wCdtZhAtZ2MOmjz3nHHvhL7RHIElRbqkUCgYEAtUAvuL2s23XAAh6BGcJMaLScVwD3tY9nRJ5wQpF+6m7v4HO30EoParkc5gSgxBLj4HiHwsvn69TjOqofTIVcWVTirLEf1OesNKFMqkq59I7pN6beUxGa+raoRU5OCfhD4dfarVXuQ+aXyr/RrK6UhmGUjBwMgZlALu69x22hcb0CgYBkOO1GPVaoIrimHYKzLH587ZP367MYj77cMiXz0A3rjbQ9zb9DsBN1PPcprRMZsHMfp7gdsrmgv1cAnV2ZAWeEPEOl7qH42rlZX9VHX40s5Ax9prEg/RB3IylYs/yddDJ/YMp/3m0USiDmTym65amb7cddaFykvWtZQlpj9ov0ww=="
+ ],
+ "certificate": [
+ "MIICozCCAYsCBgGPTXQmpjANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDDAptYXR0ZXJtb3N0MB4XDTI0MDUwNjEwMjgyOFoXDTM0MDUwNjEwMzAwOFowFTETMBEGA1UEAwwKbWF0dGVybW9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMCf+mv/K/jthSGuDfY4RkVms2G+vPWLQQVWyfmudEJVr/0rD9WmZf9B1KSqBiIQ2isAHnce/t9ArByqA96dhoezhrGOAkN4IjoBydL8feT+WT1auKHuj9ygPOTCvwlhADkBR6p8z0L3klYmAA7r2Nkzp77zYj7ofUoKZlymve+wIknttJHVKIhm83Ucybhjxw9N/k7PClWaopjSRmSSPi+J7M5v2kOCzN991GWnJAnsz2APbL0bL2vjWwJ+/mIdOaOW+js75+ocLmmdO4+It1V0fyDo/LHAMmTygpTKybFk/KruymXFr1DSqE9MKTMFfkSS8TtiQ0hRlcUEWzggAw8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYKj6Tl0zFBELqoZMOxqdgUQNk5BINy0taDwVVQ/IhIQtfw8WDvnrYL8wqqOHBd0lbQ0mDaMlkqCPHzLjBv5D2Pho8YRyVIfiPz99LXtkS8ONBloDl+ZFIrLPiSY83cELUOWbwI+f26VTNbKltmVRFjebydkemltevjGYKfZXQenFsW5c2j2qGIfEGcEvUIbwai+LECkMW7twK0XjW0TpApf04SI65Z30/j2p5uBceoR3HNVqpwkulu4oKTgg8gaIaqsbzUIFcIjrUDh1ykKVOKLZIj8NRdIIaRbonH1YLPLHDqdLvYzwj+rNxFmxbLovYKHObblPTAAjYbd5KWUfoA=="
+ ],
+ "priority": ["100"]
+ }
+ },
+ {
+ "id": "cf234e0c-7555-49cf-99c7-bef7c50ed756",
+ "name": "hmac-generated",
+ "providerId": "hmac-generated",
+ "subComponents": {},
+ "config": {
+ "kid": ["3daf79ff-b089-4ae4-be08-392660d6b818"],
+ "secret": [
+ "L_9PwDzGir1YHeM-v9Wl_A5AxifcmRLB0eyRD7lSGNgc_6RCO0oWcrA5o28X_1zMq7wBe3AnvFcuqzZr45CSbg"
+ ],
+ "priority": ["100"],
+ "algorithm": ["HS256"]
+ }
+ },
+ {
+ "id": "a584f067-cab9-4426-815c-391f366817c4",
+ "name": "aes-generated",
+ "providerId": "aes-generated",
+ "subComponents": {},
+ "config": {
+ "kid": ["259582ce-b6c1-4242-ab1a-89c51b8a16fa"],
+ "secret": ["lZ8-auqemjtb82DPk-QEeA"],
+ "priority": ["100"]
+ }
+ },
+ {
+ "id": "1b220f41-5d26-4b56-9024-f02ed1c42da5",
+ "name": "rsa-enc-generated",
+ "providerId": "rsa-enc-generated",
+ "subComponents": {},
+ "config": {
+ "privateKey": [
+ "MIIEowIBAAKCAQEAxojT5/utgBVBEXcb99b5guOquV0juPwrV/FU2kJv0QdleLVxWzsKPS/XzGIkyQbob5RM5AIXNXLfxL46SGnY7/d32qoZzVkw5F+MzRmXePaUItdS/RpC6jSWjH1MTxKyoHju7tsvgNjz7gcyXee1FrVRag/r+88EW10T/nhjJKA0BivgYXKvUOWSb5uXxs5VY17Z7k1hjir/ceQQ4Jay2RIUqHfLhgWmiayx2YMOMovOksrUYm1tqFJBJYOUODDPQBKagB/TlXXy/vT5uddDAQCLKaup8JfyXObWo67sZBMNh+oSEQWElrSRpEq5da3RRzvqjeTrVpDHeEyDa5FPYQIDAQABAoIBAB9q2bXVUxB4zJI5+hgPUe7Pnl21CokkhqtlClt7tfwrInimpeidADLMlmbGhTG2wSOfIUfXuNqBcj3NKKX68hSPYc0dq/WeDqmlIT/3s8u1oZ2d24koFbzAU66pyWCeAdx5Jr5vuyW1r2xUgTjxCOwuB1M0b9CqYvMeWKKlZHMQC6sQ0TycC+TycoOR3Vf70UjDnbvekgy8uDkRaeh+7nlQFRK5wM/7YiR5Az2uaJA63I6TXVb2ODLqMDLzCeF73LNZe5ZYdIleW6akV0c/7Lk/ldTmTJtsDdaK48hVd5B6c1GqOZ9aNA7eYn7ppw4wywQ3GhGgSy7SdFJ6k9Khaz0CgYEA52wc8V4v/t1EAG/lqERyiyHlBMPcl2w/K9lmvHwNaYTRLFrqO1MOWjvrzs9Jyzma4QxM8JBvCNeepGmUXjy2DrsaiEvkpc44hZeKTmV366Y2GhsqZw83Q9KrTtulkpAKCaGB+/Opz36J8NCtAzfT1TOJ9h1/GLbzEVzO/mwEo1sCgYEA256QRX7lkWYreDpmgQBxCVvj9mdHl/dyAswQWXSJHUlUUwrPp4HtDRD6IvrHrvmMOLPjUcfJDBjLbgDlcKsjg3IIHg6DPCYan0QeUqyKGE6QgXwAOW7mkAP7V+nGEHGFKDt6yjidKOk0ygRpwJzjtIRmfPli3Yfz7gTO2LjSwPMCgYBDFc4WEg2rvgyWa9mRO7ZQqtSu2vA5XQG6/lyPyw5ARmY0i25XIC6K7c/ftatYpCxHjL9JBTWKxKaSVqDR6JuGDasVU5rGjc9IurM3IqbuwtHwI2vZUEne+VT7mSSYJfUi8ZSyquHQ6oemLXfZ9PcNWNFnbQmI6o/vuo2upzV4/QKBgQDNXSmlgrLi0rpFU770VFhfGwlBKXSeGyoGiZGkRL/Onfp3KL3i8jCbrktpGD5PVVUU1Mn1eC9821anB7mFMNMloCUNDh8TyMHOOmFsh6Eed6DTJAQFsLly4/ZNT/IBAFY2t9/rZyM21VOisJu/HDYlYEWCCaW0Z+Pmp6JQsAuLlwKBgDWRLlaCa8+rJPgRXs1FFfwIOTMyDsWf8PoA6vIytd28YzUolfil4AaDPvyamjA7NGZIO5ZhcGTX1vnNHNYHb/zmHC3AkeuHIKSDsyTUcvjsTguyF5rddkRL0F6mb3UCCv8bKn0ts3DWX4vr2eA40hL8EklRTVB0aFL524+enT5w"
+ ],
+ "certificate": [
+ "MIICozCCAYsCBgGPTXQm0zANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDDAptYXR0ZXJtb3N0MB4XDTI0MDUwNjEwMjgyOFoXDTM0MDUwNjEwMzAwOFowFTETMBEGA1UEAwwKbWF0dGVybW9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMaI0+f7rYAVQRF3G/fW+YLjqrldI7j8K1fxVNpCb9EHZXi1cVs7Cj0v18xiJMkG6G+UTOQCFzVy38S+Okhp2O/3d9qqGc1ZMORfjM0Zl3j2lCLXUv0aQuo0lox9TE8SsqB47u7bL4DY8+4HMl3ntRa1UWoP6/vPBFtdE/54YySgNAYr4GFyr1Dlkm+bl8bOVWNe2e5NYY4q/3HkEOCWstkSFKh3y4YFpomssdmDDjKLzpLK1GJtbahSQSWDlDgwz0ASmoAf05V18v70+bnXQwEAiymrqfCX8lzm1qOu7GQTDYfqEhEFhJa0kaRKuXWt0Uc76o3k61aQx3hMg2uRT2ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEANa0lkttYrlutce6OACDMj7KAzH+zNFWxG+A7ayw10mDXq+9SIrrwzfcQVhCoKoWabl+OSe7kQdu5RwWxRZ74j6BD2lH3ITlDbuMQZnUlBmxJvMsUNofFhnCJ+Mt59IKQfLORXFOx5fF8ds8uF6KkR18tKX07pNjYD7GKWY61TmBKHY6oq4svr4FmROqcdww1OEcwhZytaEA5T0kGo4vlkGTR5v7xigjWEFlsR8HA1QAUTOoUECq7WEmQC7JUOXprMevedHrL9BD/PXmZda0NyPf4xWt3Lnoay1g6OGmhcW8BiLFmPPnq+YQE3t//tg9EcHoysLBpTC9vTFxS5JFaJA=="
+ ],
+ "priority": ["100"],
+ "algorithm": ["RSA-OAEP"]
+ }
+ }
+ ]
+ },
+ "internationalizationEnabled": false,
+ "supportedLocales": [],
+ "authenticationFlows": [
+ {
+ "id": "3e3fc387-7ba6-4bbb-95ad-d637c398de02",
+ "alias": "Account verification options",
+ "description": "Method with which to verity the existing account",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "idp-email-verification",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "ALTERNATIVE",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Verify Existing Account by Re-authentication",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "9537a3f9-40f9-4970-abc3-29420d62c0af",
+ "alias": "Browser - Conditional OTP",
+ "description": "Flow to determine if the OTP is required for the authentication",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "auth-otp-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "e3890212-5508-4c4f-886c-42292b71383e",
+ "alias": "Direct Grant - Conditional OTP",
+ "description": "Flow to determine if the OTP is required for the authentication",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "direct-grant-validate-otp",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "0ad2c5de-ae22-409a-9ce3-a91d2e27d4d4",
+ "alias": "First broker login - Conditional OTP",
+ "description": "Flow to determine if the OTP is required for the authentication",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "auth-otp-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "1ca93d86-2158-4a4b-8f4c-ad2c341551c0",
+ "alias": "Handle Existing Account",
+ "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "idp-confirm-link",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Account verification options",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "855c44a5-25de-4e2a-9055-d7820c486aa8",
+ "alias": "Reset - Conditional OTP",
+ "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "reset-otp",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "5e2179fb-85bf-4bb7-a34c-df270f5b538e",
+ "alias": "User creation or linking",
+ "description": "Flow for the existing/non-existing user alternatives",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticatorConfig": "create unique user config",
+ "authenticator": "idp-create-user-if-unique",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "ALTERNATIVE",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Handle Existing Account",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "b07dae46-dffa-469a-addd-155dcc454b8e",
+ "alias": "Verify Existing Account by Re-authentication",
+ "description": "Reauthentication of existing account",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "idp-username-password-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "First broker login - Conditional OTP",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "5502a151-e047-4385-a0d8-115f23e9f64a",
+ "alias": "browser",
+ "description": "browser based authentication",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "auth-cookie",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "auth-spnego",
+ "authenticatorFlow": false,
+ "requirement": "DISABLED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "identity-provider-redirector",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 25,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "ALTERNATIVE",
+ "priority": 30,
+ "autheticatorFlow": true,
+ "flowAlias": "forms",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "567a6082-f98f-4f6b-9d98-f8c53d58626d",
+ "alias": "clients",
+ "description": "Base authentication for clients",
+ "providerId": "client-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "client-secret",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "client-jwt",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "client-secret-jwt",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 30,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "client-x509",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 40,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "7b2672cd-462a-43ad-9b7f-4074ee7380ee",
+ "alias": "direct grant",
+ "description": "OpenID Connect Resource Owner Grant",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "direct-grant-validate-username",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "direct-grant-validate-password",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 30,
+ "autheticatorFlow": true,
+ "flowAlias": "Direct Grant - Conditional OTP",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "8478b453-df05-46a6-aac9-5eb08885a447",
+ "alias": "docker auth",
+ "description": "Used by Docker clients to authenticate against the IDP",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "docker-http-basic-authenticator",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "cbd75a2b-8dcc-4f63-a2b9-2d30ed937eab",
+ "alias": "first broker login",
+ "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticatorConfig": "review profile config",
+ "authenticator": "idp-review-profile",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "User creation or linking",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "86121d61-c8e6-4758-8c45-55a99291c877",
+ "alias": "forms",
+ "description": "Username, password, otp and other auth forms.",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "auth-username-password-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Browser - Conditional OTP",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "fd722202-55f6-42fc-813c-9d568280dd00",
+ "alias": "registration",
+ "description": "registration flow",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "registration-page-form",
+ "authenticatorFlow": true,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": true,
+ "flowAlias": "registration form",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "e604d749-b65d-4709-b7fe-683043322069",
+ "alias": "registration form",
+ "description": "registration form",
+ "providerId": "form-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "registration-user-creation",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "registration-password-action",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 50,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "registration-recaptcha-action",
+ "authenticatorFlow": false,
+ "requirement": "DISABLED",
+ "priority": 60,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "34ed29ea-ff5a-48f5-89fb-55e7b0d08db5",
+ "alias": "reset credentials",
+ "description": "Reset credentials for a user if they forgot their password or something",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "reset-credentials-choose-user",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "reset-credential-email",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "reset-password",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 30,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 40,
+ "autheticatorFlow": true,
+ "flowAlias": "Reset - Conditional OTP",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "9c45a256-329f-40f5-947c-060be56c5fe3",
+ "alias": "saml ecp",
+ "description": "SAML ECP Profile Authentication Flow",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "http-basic-authenticator",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ }
+ ],
+ "authenticatorConfig": [
+ {
+ "id": "f6497f9b-75bb-4e4a-97fb-502a77c95594",
+ "alias": "create unique user config",
+ "config": {
+ "require.password.update.after.registration": "false"
+ }
+ },
+ {
+ "id": "66b63196-6ebc-41cf-a462-e71dfe5baf84",
+ "alias": "review profile config",
+ "config": {
+ "update.profile.on.first.login": "missing"
+ }
+ }
+ ],
+ "requiredActions": [
+ {
+ "alias": "CONFIGURE_TOTP",
+ "name": "Configure OTP",
+ "providerId": "CONFIGURE_TOTP",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 10,
+ "config": {}
+ },
+ {
+ "alias": "TERMS_AND_CONDITIONS",
+ "name": "Terms and Conditions",
+ "providerId": "TERMS_AND_CONDITIONS",
+ "enabled": false,
+ "defaultAction": false,
+ "priority": 20,
+ "config": {}
+ },
+ {
+ "alias": "UPDATE_PASSWORD",
+ "name": "Update Password",
+ "providerId": "UPDATE_PASSWORD",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 30,
+ "config": {}
+ },
+ {
+ "alias": "UPDATE_PROFILE",
+ "name": "Update Profile",
+ "providerId": "UPDATE_PROFILE",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 40,
+ "config": {}
+ },
+ {
+ "alias": "VERIFY_EMAIL",
+ "name": "Verify Email",
+ "providerId": "VERIFY_EMAIL",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 50,
+ "config": {}
+ },
+ {
+ "alias": "delete_account",
+ "name": "Delete Account",
+ "providerId": "delete_account",
+ "enabled": false,
+ "defaultAction": false,
+ "priority": 60,
+ "config": {}
+ },
+ {
+ "alias": "webauthn-register",
+ "name": "Webauthn Register",
+ "providerId": "webauthn-register",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 70,
+ "config": {}
+ },
+ {
+ "alias": "webauthn-register-passwordless",
+ "name": "Webauthn Register Passwordless",
+ "providerId": "webauthn-register-passwordless",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 80,
+ "config": {}
+ },
+ {
+ "alias": "update_user_locale",
+ "name": "Update User Locale",
+ "providerId": "update_user_locale",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 1000,
+ "config": {}
+ }
+ ],
+ "browserFlow": "browser",
+ "registrationFlow": "registration",
+ "directGrantFlow": "direct grant",
+ "resetCredentialsFlow": "reset credentials",
+ "clientAuthenticationFlow": "clients",
+ "dockerAuthenticationFlow": "docker auth",
+ "attributes": {
+ "cibaBackchannelTokenDeliveryMode": "poll",
+ "cibaExpiresIn": "120",
+ "cibaAuthRequestedUserHint": "login_hint",
+ "oauth2DeviceCodeLifespan": "600",
+ "clientOfflineSessionMaxLifespan": "0",
+ "oauth2DevicePollingInterval": "5",
+ "clientSessionIdleTimeout": "0",
+ "parRequestUriLifespan": "60",
+ "clientSessionMaxLifespan": "0",
+ "clientOfflineSessionIdleTimeout": "0",
+ "cibaInterval": "5",
+ "realmReusableOtpCode": "false"
+ },
+ "keycloakVersion": "23.0.7",
+ "userManagedAccessAllowed": false,
+ "clientProfiles": {
+ "profiles": []
+ },
+ "clientPolicies": {
+ "policies": []
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/containers/assets/postgres.conf b/e2e-tests/playwright/lib/src/containers/assets/postgres.conf
new file mode 100644
index 000000000000..cb177bb85707
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/assets/postgres.conf
@@ -0,0 +1,9 @@
+# Copied from server/build/docker/postgres.conf on 2026-07-19.
+max_connections = 500
+listen_addresses = '*'
+fsync = off
+full_page_writes = off
+default_text_search_config = 'pg_catalog.english'
+commit_delay=1000
+logging_collector=off
+password_encryption = 'scram-sha-256'
diff --git a/e2e-tests/playwright/lib/src/containers/assets/webhook/Dockerfile.webhook b/e2e-tests/playwright/lib/src/containers/assets/webhook/Dockerfile.webhook
new file mode 100644
index 000000000000..2698cdc4697f
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/assets/webhook/Dockerfile.webhook
@@ -0,0 +1,20 @@
+# webhook_serve.js, utils/webhook_utils.js, and tests/plugins/post_message_as.js are vendored
+# copies of e2e-tests/cypress's files of the same name (shared sidecar, also used by Cypress) —
+# copied on 2026-07-20. Dependencies match e2e-tests/.ci/server.generate.sh's "playwright" Compose
+# service, just installed at build time instead of on every container start.
+FROM node:24-alpine
+
+WORKDIR /webhook
+
+RUN npm init -y >/dev/null && \
+ npm install express@5.1.0 axios@1.11.0 client-oauth2@github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49
+
+COPY webhook_serve.js ./webhook_serve.js
+COPY utils/ ./utils/
+COPY tests/plugins/post_message_as.js ./tests/plugins/post_message_as.js
+
+RUN chown -R node:node /webhook
+USER node
+
+EXPOSE 3000
+CMD ["node", "webhook_serve.js"]
diff --git a/e2e-tests/playwright/lib/src/containers/assets/webhook/tests/plugins/post_message_as.js b/e2e-tests/playwright/lib/src/containers/assets/webhook/tests/plugins/post_message_as.js
new file mode 100644
index 000000000000..3ec297121f27
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/assets/webhook/tests/plugins/post_message_as.js
@@ -0,0 +1,44 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+const axios = require('axios');
+
+module.exports = async ({sender, message, channelId, rootId, createAt = 0, baseUrl}) => {
+ const loginResponse = await axios({
+ url: `${baseUrl}/api/v4/users/login`,
+ headers: {'X-Requested-With': 'XMLHttpRequest'},
+ method: 'post',
+ data: {login_id: sender.username, password: sender.password},
+ });
+
+ const setCookie = loginResponse.headers['set-cookie'];
+ let cookieString = '';
+ setCookie.forEach((cookie) => {
+ const nameAndValue = cookie.split(';')[0];
+ cookieString += nameAndValue + ';';
+ });
+
+ let response;
+ try {
+ response = await axios({
+ url: `${baseUrl}/api/v4/posts`,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest',
+ Cookie: cookieString,
+ },
+ method: 'post',
+ data: {
+ channel_id: channelId,
+ message,
+ type: '',
+ create_at: createAt,
+ root_id: rootId,
+ },
+ });
+ } catch (err) {
+ expect(Boolean(err)).to.equal(false);
+ }
+
+ return {status: response.status, data: response.data};
+};
diff --git a/e2e-tests/playwright/lib/src/containers/assets/webhook/utils/webhook_utils.js b/e2e-tests/playwright/lib/src/containers/assets/webhook/utils/webhook_utils.js
new file mode 100644
index 000000000000..0925d6aa3140
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/assets/webhook/utils/webhook_utils.js
@@ -0,0 +1,837 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+// Helper function to create dialog base structure
+function createDialog(triggerId, webhookBaseUrl, dialogConfig) {
+ const baseDialog = {
+ trigger_id: triggerId,
+ url: `${webhookBaseUrl}/dialog_submit`,
+ dialog: {
+ callback_id: dialogConfig.callback_id,
+ title: dialogConfig.title,
+ submit_label: dialogConfig.submit_label || 'Submit',
+ notify_on_cancel: true,
+ ...dialogConfig.dialog_props,
+ elements: dialogConfig.elements || [],
+ },
+ };
+
+ if (dialogConfig.icon_url) {
+ baseDialog.dialog.icon_url = dialogConfig.icon_url;
+ }
+
+ if (dialogConfig.introduction_text) {
+ baseDialog.dialog.introduction_text = dialogConfig.introduction_text;
+ }
+
+ if (dialogConfig.state) {
+ baseDialog.dialog.state = dialogConfig.state;
+ }
+
+ if (dialogConfig.source_url) {
+ baseDialog.dialog.source_url = dialogConfig.source_url;
+ }
+
+ return baseDialog;
+}
+
+// Helper function to create form response structure
+function createFormResponse(formConfig) {
+ return {
+ callback_id: formConfig.callback_id,
+ title: formConfig.title,
+ submit_label: formConfig.submit_label || 'Submit',
+ notify_on_cancel: true,
+ elements: formConfig.elements || [],
+ ...formConfig.form_props,
+ };
+}
+
+// Helper function to create common form elements
+function createElement(type, config) {
+ const baseElement = {
+ display_name: config.display_name,
+ name: config.name,
+ type,
+ optional: config.optional || false,
+ };
+
+ if (config.placeholder) {
+ baseElement.placeholder = config.placeholder;
+ }
+ if (config.help_text) {
+ baseElement.help_text = config.help_text;
+ }
+ if (config.default) {
+ baseElement.default = config.default;
+ }
+ if (config.subtype) {
+ baseElement.subtype = config.subtype;
+ }
+ if (config.min_length) {
+ baseElement.min_length = config.min_length;
+ }
+ if (config.max_length) {
+ baseElement.max_length = config.max_length;
+ }
+ if (config.data_source) {
+ baseElement.data_source = config.data_source;
+ }
+ if (config.options) {
+ baseElement.options = config.options;
+ }
+ if (config.refresh) {
+ baseElement.refresh = config.refresh;
+ }
+ if (config.action_button) {
+ baseElement.action_button = config.action_button;
+ }
+
+ return baseElement;
+}
+
+// Standard icon URL
+const STANDARD_ICON = 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png';
+
+// Dialog configurations
+const DIALOG_CONFIGS = {
+ full: {
+ callback_id: 'somecallbackid',
+ title: 'Title for Full Dialog Test',
+ icon_url: STANDARD_ICON,
+ elements: [
+ createElement('text', {
+ display_name: 'Display Name',
+ name: 'realname',
+ default: 'default text',
+ placeholder: 'placeholder',
+ help_text: 'This a regular input in an interactive dialog triggered by a test integration.',
+ }),
+ createElement('text', {
+ display_name: 'Email',
+ name: 'someemail',
+ subtype: 'email',
+ placeholder: 'placeholder@bladekick.com',
+ help_text: 'This a regular email input in an interactive dialog triggered by a test integration.',
+ }),
+ createElement('text', {display_name: 'Number', name: 'somenumber', subtype: 'number'}),
+ createElement('text', {
+ display_name: 'Password',
+ name: 'somepassword',
+ subtype: 'password',
+ default: 'p@ssW0rd',
+ placeholder: 'placeholder',
+ help_text: 'This a password input in an interactive dialog triggered by a test integration.',
+ optional: true,
+ }),
+ createElement('textarea', {
+ display_name: 'Display Name Long Text Area',
+ name: 'realnametextarea',
+ placeholder: 'placeholder',
+ optional: true,
+ min_length: 5,
+ max_length: 100,
+ }),
+ createElement('select', {
+ display_name: 'User Selector',
+ name: 'someuserselector',
+ placeholder: 'Select a user...',
+ data_source: 'users',
+ }),
+ createElement('select', {
+ display_name: 'Channel Selector',
+ name: 'somechannelselector',
+ placeholder: 'Select a channel...',
+ help_text: 'Choose a channel from the list.',
+ data_source: 'channels',
+ optional: true,
+ }),
+ createElement('select', {
+ display_name: 'Option Selector',
+ name: 'someoptionselector',
+ placeholder: 'Select an option...',
+ options: [
+ {text: 'Option1', value: 'opt1'},
+ {text: 'Option2', value: 'opt2'},
+ {text: 'Option3', value: 'opt3'},
+ ],
+ }),
+ createElement('radio', {
+ display_name: 'Radio Option Selector',
+ name: 'someradiooptions',
+ help_text: '',
+ options: [
+ {text: 'Engineering', value: 'engineering'},
+ {text: 'Sales', value: 'sales'},
+ ],
+ }),
+ createElement('bool', {
+ display_name: 'Boolean Selector',
+ name: 'boolean_input',
+ placeholder: 'Was this modal helpful?',
+ default: 'True',
+ optional: true,
+ help_text: 'This is the help text',
+ }),
+ ],
+ dialog_props: {state: 'somestate'},
+ },
+
+ simple: {
+ callback_id: 'somecallbackid',
+ title: 'Title for Dialog Test without elements',
+ icon_url: STANDARD_ICON,
+ submit_label: 'Submit Test',
+ dialog_props: {state: 'somestate'},
+ },
+
+ userAndChannel: {
+ callback_id: 'somecallbackid',
+ title: 'Title for Dialog Test with user and channel element',
+ icon_url: STANDARD_ICON,
+ submit_label: 'Submit Test',
+ elements: [
+ createElement('select', {
+ display_name: 'User Selector',
+ name: 'someuserselector',
+ placeholder: 'Select a user...',
+ data_source: 'users',
+ }),
+ createElement('select', {
+ display_name: 'Channel Selector',
+ name: 'somechannelselector',
+ placeholder: 'Select a channel...',
+ help_text: 'Choose a channel from the list.',
+ data_source: 'channels',
+ optional: true,
+ }),
+ ],
+ dialog_props: {state: 'somestate'},
+ },
+
+ boolean: {
+ callback_id: 'somecallbackid',
+ title: 'Title for Dialog Test with boolean element',
+ icon_url: STANDARD_ICON,
+ submit_label: 'Submit Test',
+ elements: [
+ createElement('bool', {
+ display_name: 'Boolean Selector',
+ name: 'boolean_input',
+ placeholder: 'Was this modal helpful?',
+ default: 'True',
+ optional: true,
+ help_text: 'This is the help text',
+ }),
+ ],
+ dialog_props: {state: 'somestate'},
+ },
+
+ fieldRefresh: {
+ callback_id: 'field_refresh_callback',
+ title: 'Field Refresh Demo',
+ introduction_text: 'Enter project name then select type to see different fields',
+ elements: [
+ createElement('text', {
+ display_name: 'Project Name',
+ name: 'project_name',
+ placeholder: 'Enter project name',
+ }),
+ createElement('select', {
+ display_name: 'Project Type',
+ name: 'project_type',
+ refresh: true,
+ placeholder: 'Select project type...',
+ options: [
+ {text: 'Web Application', value: 'web'},
+ {text: 'Mobile App', value: 'mobile'},
+ {text: 'API Service', value: 'api'},
+ ],
+ }),
+ ],
+ },
+
+ multistepStep1: {
+ callback_id: 'multistep_callback',
+ title: 'Step 1 - Personal Info',
+ introduction_text: 'Multi-step registration - Step 1 of 3',
+ submit_label: 'Next Step',
+ elements: [
+ createElement('text', {
+ display_name: 'First Name',
+ name: 'first_name',
+ placeholder: 'Enter your first name',
+ }),
+ createElement('text', {
+ display_name: 'Email',
+ name: 'email',
+ subtype: 'email',
+ placeholder: 'Enter your email address',
+ }),
+ ],
+ dialog_props: {state: 'step1'},
+ },
+
+ multistepStep2: {
+ callback_id: 'multistep_callback',
+ title: 'Step 2 - Work Info',
+ introduction_text: 'Multi-step registration - Step 2 of 3',
+ submit_label: 'Next Step',
+ elements: [
+ createElement('select', {
+ display_name: 'Department',
+ name: 'department',
+ placeholder: 'Select department...',
+ options: [
+ {text: 'Engineering', value: 'engineering'},
+ {text: 'Marketing', value: 'marketing'},
+ {text: 'Sales', value: 'sales'},
+ ],
+ }),
+ createElement('radio', {
+ display_name: 'Experience Level',
+ name: 'experience_level',
+ options: [
+ {text: 'Junior', value: 'junior'},
+ {text: 'Mid-level', value: 'mid'},
+ {text: 'Senior', value: 'senior'},
+ ],
+ }),
+ ],
+ form_props: {state: 'step2'},
+ },
+
+ multistepStep3: {
+ callback_id: 'multistep_callback',
+ title: 'Step 3 - Final Details',
+ introduction_text: 'Multi-step registration - Step 3 of 3',
+ submit_label: 'Complete Registration',
+ elements: [
+ createElement('textarea', {
+ display_name: 'Comments',
+ name: 'comments',
+ placeholder: 'Any additional comments...',
+ optional: true,
+ }),
+ createElement('bool', {display_name: 'Terms & Conditions', name: 'terms_accepted'}),
+ ],
+ form_props: {state: 'step3'},
+ },
+
+ actionButtonParent: {
+ callback_id: 'action_button_parent_callback',
+ title: 'Parent Dialog with Action Button',
+ elements: [],
+ },
+
+ actionButtonChild: {
+ callback_id: 'child_callback',
+ title: 'Child Dialog',
+ elements: [
+ createElement('text', {
+ display_name: 'Child Input',
+ name: 'child_input',
+ placeholder: 'Enter value',
+ optional: true,
+ }),
+ ],
+ },
+};
+
+// Public API functions
+function getFullDialog(triggerId, webhookBaseUrl) {
+ return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.full);
+}
+
+function getSimpleDialog(triggerId, webhookBaseUrl) {
+ return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.simple);
+}
+
+function getUserAndChannelDialog(triggerId, webhookBaseUrl) {
+ return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.userAndChannel);
+}
+
+function getBooleanDialog(triggerId, webhookBaseUrl) {
+ return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.boolean);
+}
+
+function getFieldRefreshDialog(triggerId, webhookBaseUrl) {
+ const config = {...DIALOG_CONFIGS.fieldRefresh};
+ config.source_url = `${webhookBaseUrl}/field_refresh_source`;
+ return createDialog(triggerId, webhookBaseUrl, config);
+}
+
+function getMultistepStep1Dialog(triggerId, webhookBaseUrl) {
+ return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.multistepStep1);
+}
+
+function getMultistepStep2Dialog(triggerId, webhookBaseUrl) {
+ const config = {...DIALOG_CONFIGS.multistepStep2};
+ config.dialog_props = {url: `${webhookBaseUrl}/dialog_submit`, ...config.form_props};
+ return createFormResponse(config);
+}
+
+function getMultistepStep3Dialog(triggerId, webhookBaseUrl) {
+ const config = {...DIALOG_CONFIGS.multistepStep3};
+ config.dialog_props = {url: `${webhookBaseUrl}/dialog_submit`, ...config.form_props};
+ return createFormResponse(config);
+}
+
+function getMultiSelectDialog(triggerId, webhookBaseUrl, includeDefaults = false) {
+ return {
+ trigger_id: triggerId,
+ url: `${webhookBaseUrl}/dialog_submit`,
+ dialog: {
+ callback_id: 'somecallbackid',
+ title: 'Title for Dialog Test with multiselect elements',
+ icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
+ submit_label: 'Submit Multiselect Test',
+ notify_on_cancel: true,
+ state: 'somestate',
+ elements: [
+ {
+ display_name: 'Multi Option Selector',
+ name: 'multiselect_options',
+ type: 'select',
+ multiselect: true,
+ default: includeDefaults ? 'opt1,opt3' : '',
+ placeholder: 'Select multiple options...',
+ help_text: 'You can select multiple options from this list.',
+ optional: false,
+ min_length: 0,
+ max_length: 0,
+ data_source: '',
+ options: [
+ {
+ text: 'Engineering',
+ value: 'opt1',
+ },
+ {
+ text: 'Sales',
+ value: 'opt2',
+ },
+ {
+ text: 'Marketing',
+ value: 'opt3',
+ },
+ {
+ text: 'Support',
+ value: 'opt4',
+ },
+ {
+ text: 'Product',
+ value: 'opt5',
+ },
+ ],
+ },
+ {
+ display_name: 'Multi User Selector',
+ name: 'multiselect_users',
+ type: 'select',
+ multiselect: true,
+ default: '',
+ placeholder: 'Select multiple users...',
+ help_text: 'Choose multiple users from the team.',
+ optional: false,
+ min_length: 0,
+ max_length: 0,
+ data_source: 'users',
+ options: null,
+ },
+ {
+ display_name: 'Single Option Selector',
+ name: 'single_select_options',
+ type: 'select',
+ multiselect: false,
+ default: includeDefaults ? 'single2' : '',
+ placeholder: 'Select one option...',
+ help_text: 'This is a regular single-select for comparison.',
+ optional: false,
+ min_length: 0,
+ max_length: 0,
+ data_source: '',
+ options: [
+ {
+ text: 'Single Option 1',
+ value: 'single1',
+ },
+ {
+ text: 'Single Option 2',
+ value: 'single2',
+ },
+ {
+ text: 'Single Option 3',
+ value: 'single3',
+ },
+ ],
+ },
+ ],
+ },
+ };
+}
+
+function getDynamicSelectDialog(triggerId, webhookBaseUrl) {
+ return {
+ trigger_id: triggerId,
+ url: `${webhookBaseUrl}/dialog_submit`,
+ dialog: {
+ callback_id: 'somecallbackid',
+ title: 'Title for Dialog Test with dynamic select element',
+ icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
+ submit_label: 'Submit Dynamic Select Test',
+ notify_on_cancel: true,
+ state: 'somestate',
+ elements: [
+ {
+ display_name: 'Dynamic Role Selector',
+ name: 'dynamic_role_selector',
+ type: 'select',
+ data_source: 'dynamic',
+ data_source_url: `${webhookBaseUrl}/dynamic_select_source`,
+ default: '',
+ placeholder: 'Search for a role...',
+ help_text: 'Start typing to search for available roles. Options are loaded dynamically.',
+ optional: false,
+ min_length: 0,
+ max_length: 0,
+ },
+ {
+ display_name: 'Optional Dynamic Selector',
+ name: 'optional_dynamic_selector',
+ type: 'select',
+ data_source: 'dynamic',
+ data_source_url: `${webhookBaseUrl}/dynamic_select_source`,
+ default: 'backend_eng',
+ placeholder: 'Search for another role...',
+ help_text: 'This field is optional and has a default value.',
+ optional: true,
+ min_length: 0,
+ max_length: 0,
+ },
+ ],
+ },
+ };
+}
+
+// Basic date field test - MM-T2530A
+function getBasicDateDialog(triggerId, webhookBaseUrl) {
+ return {
+ trigger_id: triggerId,
+ url: `${webhookBaseUrl}/datetime_dialog_submit`,
+ dialog: {
+ callback_id: 'basic_date_callback',
+ title: 'DateTime Fields Test',
+ icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
+ elements: [
+ {
+ display_name: 'Event Date',
+ name: 'event_date',
+ type: 'date',
+ default: '',
+ placeholder: 'Select a date',
+ help_text: 'Select the date for your event',
+ optional: false,
+ },
+ ],
+ submit_label: 'Submit',
+ notify_on_cancel: true,
+ state: 'datetime_state',
+ },
+ };
+}
+
+// Basic datetime field test - MM-T2530B
+function getBasicDateTimeDialog(triggerId, webhookBaseUrl) {
+ return {
+ trigger_id: triggerId,
+ url: `${webhookBaseUrl}/datetime_dialog_submit`,
+ dialog: {
+ callback_id: 'basic_datetime_callback',
+ title: 'DateTime Fields Test',
+ icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
+ elements: [
+ {
+ display_name: 'Event Date',
+ name: 'event_date',
+ type: 'date',
+ default: '',
+ placeholder: 'Select a date',
+ help_text: 'Select the date for your event',
+ optional: false,
+ },
+ {
+ display_name: 'Meeting Time',
+ name: 'meeting_time',
+ type: 'datetime',
+ default: '',
+ placeholder: 'Select date and time',
+ help_text: 'Select the date and time for your meeting',
+ optional: false,
+ time_interval: 60,
+ },
+ ],
+ submit_label: 'Submit',
+ notify_on_cancel: true,
+ state: 'datetime_state',
+ },
+ };
+}
+
+// Date field with min_date constraint - MM-T2530C
+function getMinDateConstraintDialog(triggerId, webhookBaseUrl) {
+ return {
+ trigger_id: triggerId,
+ url: `${webhookBaseUrl}/datetime_dialog_submit`,
+ dialog: {
+ callback_id: 'mindate_callback',
+ title: 'DateTime Fields Test',
+ icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
+ elements: [
+ {
+ display_name: 'Future Date Only',
+ name: 'future_date',
+ type: 'date',
+ default: '',
+ placeholder: 'Select a future date',
+ help_text: 'Must be today or later',
+ optional: true,
+ min_date: 'today',
+ },
+ ],
+ submit_label: 'Submit',
+ notify_on_cancel: true,
+ state: 'datetime_state',
+ },
+ };
+}
+
+// DateTime field with custom time interval - MM-T2530D
+function getCustomIntervalDialog(triggerId, webhookBaseUrl) {
+ return {
+ trigger_id: triggerId,
+ url: `${webhookBaseUrl}/datetime_dialog_submit`,
+ dialog: {
+ callback_id: 'interval_callback',
+ title: 'DateTime Fields Test',
+ icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
+ elements: [
+ {
+ display_name: 'Custom Interval Time',
+ name: 'interval_time',
+ type: 'datetime',
+ default: '',
+ placeholder: 'Select time (30min intervals)',
+ help_text: 'Time picker with 30-minute intervals',
+ optional: true,
+ time_interval: 30,
+ },
+ ],
+ submit_label: 'Submit',
+ notify_on_cancel: true,
+ state: 'datetime_state',
+ },
+ };
+}
+
+// Relative date values test - MM-T2530F
+function getRelativeDateDialog(triggerId, webhookBaseUrl) {
+ return {
+ trigger_id: triggerId,
+ url: `${webhookBaseUrl}/datetime_dialog_submit`,
+ dialog: {
+ callback_id: 'relative_callback',
+ title: 'DateTime Fields Test',
+ icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
+ elements: [
+ {
+ display_name: 'Relative Date Example',
+ name: 'relative_date',
+ type: 'date',
+ default: 'today',
+ placeholder: 'Today by default',
+ help_text: 'Defaults to today using relative date',
+ optional: true,
+ },
+ {
+ display_name: 'Relative DateTime Example',
+ name: 'relative_datetime',
+ type: 'datetime',
+ default: '+1d',
+ placeholder: 'Tomorrow by default',
+ help_text: 'Defaults to tomorrow using relative date',
+ optional: true,
+ },
+ ],
+ submit_label: 'Submit',
+ notify_on_cancel: true,
+ state: 'datetime_state',
+ },
+ };
+}
+
+// Legacy function for backward compatibility - returns basic datetime dialog
+function getDateTimeDialog(triggerId, webhookBaseUrl) {
+ return getBasicDateTimeDialog(triggerId, webhookBaseUrl);
+}
+
+function getTimezoneManualDialog(triggerId, webhookBaseUrl) {
+ return createDialog(triggerId, webhookBaseUrl, {
+ callback_id: 'timezone_manual',
+ title: 'Timezone & Manual Entry Demo',
+ introduction_text:
+ '**Timezone & Manual Entry Demo**\n\n' +
+ 'This dialog demonstrates timezone support and manual time entry features.',
+ elements: [
+ {
+ display_name: 'Your Local Time (Manual Entry)',
+ name: 'local_manual',
+ type: 'datetime',
+ help_text: 'Type any time: 9am, 14:30, 3:45pm - no rounding',
+ datetime_config: {
+ manual_time_entry: true,
+ },
+ optional: true,
+ },
+ {
+ display_name: 'London Office Hours (Dropdown)',
+ name: 'london_dropdown',
+ type: 'datetime',
+ help_text: 'Times shown in GMT - select from 60 min intervals',
+ datetime_config: {
+ location_timezone: 'Europe/London',
+ time_interval: 60,
+ },
+ optional: true,
+ },
+ {
+ display_name: 'London Office Hours (Manual Entry)',
+ name: 'london_manual',
+ type: 'datetime',
+ help_text: 'Type time in GMT: 9am, 14:30, 3:45pm - no rounding',
+ datetime_config: {
+ location_timezone: 'Europe/London',
+ manual_time_entry: true,
+ },
+ optional: true,
+ },
+ ],
+ });
+}
+
+function getFileUploadDialog(triggerId, webhookBaseUrl) {
+ return {
+ trigger_id: triggerId,
+ url: `${webhookBaseUrl}/dialog_submit`,
+ dialog: {
+ callback_id: 'somecallbackid',
+ title: 'Title for Dialog Test with file upload element',
+ icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
+ submit_label: 'Submit File Upload Test',
+ notify_on_cancel: true,
+ state: 'somestate',
+ elements: [
+ {
+ display_name: 'Upload Single Document',
+ name: 'single_document',
+ type: 'file',
+ placeholder: 'Select one document...',
+ help_text: 'Upload a single document (replaces previous selection).',
+ optional: false,
+ },
+ {
+ display_name: 'Upload Multiple Files',
+ name: 'multiple_files',
+ type: 'file',
+ allow_multiple: true,
+ placeholder: 'Select multiple files...',
+ help_text: 'Upload multiple files (can select and add more).',
+ optional: false,
+ },
+ {
+ display_name: 'Description',
+ name: 'description',
+ type: 'textarea',
+ subtype: '',
+ default: '',
+ placeholder: 'Describe the uploaded files...',
+ help_text: 'Provide a description for the uploaded files.',
+ optional: true,
+ min_length: 0,
+ max_length: 500,
+ data_source: '',
+ options: null,
+ },
+ ],
+ },
+ };
+}
+
+function getActionButtonParentDialog(triggerId, webhookBaseUrl) {
+ const config = {
+ ...DIALOG_CONFIGS.actionButtonParent,
+ elements: [
+ createElement('text', {
+ display_name: 'Your Name',
+ name: 'your_name',
+ placeholder: 'Enter your name',
+ optional: true,
+ }),
+
+ // Two action buttons on the same dialog. Each carries a distinct
+ // context.source so the child dialog can reflect which one was pressed.
+ createElement('action_button', {
+ display_name: 'Open Details',
+ name: 'open_details',
+ action_button: {
+ url: `${webhookBaseUrl}/dialog/open_child`,
+ context: {source: 'Details'},
+ },
+ }),
+ createElement('action_button', {
+ display_name: 'Open Summary',
+ name: 'open_summary',
+ action_button: {
+ url: `${webhookBaseUrl}/dialog/open_child`,
+ context: {source: 'Summary'},
+ },
+ }),
+ ],
+ };
+ return createDialog(triggerId, webhookBaseUrl, config);
+}
+
+// `source` comes from the pressed action button's context.source and is reflected
+// in the child dialog's title and introduction text, so a test can verify which
+// button opened it.
+function getActionButtonChildDialog(triggerId, webhookBaseUrl, source) {
+ const label = source || 'Unknown';
+ const config = {
+ ...DIALOG_CONFIGS.actionButtonChild,
+ title: `${label} Dialog`,
+ introduction_text: `This child dialog was opened from the "${label}" action button.`,
+ };
+ return createDialog(triggerId, webhookBaseUrl, config);
+}
+
+module.exports = {
+ getFullDialog,
+ getSimpleDialog,
+ getUserAndChannelDialog,
+ getBooleanDialog,
+ getFieldRefreshDialog,
+ getMultistepStep1Dialog,
+ getMultistepStep2Dialog,
+ getMultistepStep3Dialog,
+ getMultiSelectDialog,
+ getDynamicSelectDialog,
+ getDateTimeDialog,
+ getBasicDateDialog,
+ getBasicDateTimeDialog,
+ getMinDateConstraintDialog,
+ getCustomIntervalDialog,
+ getRelativeDateDialog,
+ getTimezoneManualDialog,
+ getFileUploadDialog,
+ getActionButtonParentDialog,
+ getActionButtonChildDialog,
+};
diff --git a/e2e-tests/playwright/lib/src/containers/assets/webhook/webhook_serve.js b/e2e-tests/playwright/lib/src/containers/assets/webhook/webhook_serve.js
new file mode 100644
index 000000000000..cbb828bf0fd4
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/assets/webhook/webhook_serve.js
@@ -0,0 +1,770 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+/* eslint-disable no-console */
+
+const express = require('express');
+const axios = require('axios');
+const ClientOAuth2 = require('client-oauth2');
+
+const webhookUtils = require('./utils/webhook_utils');
+const postMessageAs = require('./tests/plugins/post_message_as');
+
+const port = 3000;
+
+const server = express();
+server.use(express.json());
+server.use(express.urlencoded({extended: true}));
+
+process.title = process.argv[2];
+
+server.get('/', ping);
+server.post('/setup', doSetup);
+server.post('/message_menus', postMessageMenus);
+server.post('/dialog_request', onDialogRequest);
+server.post('/simple_dialog_request', onSimpleDialogRequest);
+server.post('/user_and_channel_dialog_request', onUserAndChannelDialogRequest);
+server.post('/dialog_submit', onDialogSubmit);
+server.post('/boolean_dialog_request', onBooleanDialogRequest);
+server.post('/multiselect_dialog_request', onMultiSelectDialogRequest);
+server.post('/dynamic_select_dialog_request', onDynamicSelectDialogRequest);
+server.post('/file_upload_dialog_request', onFileUploadDialogRequest);
+server.post('/dynamic_select_source', onDynamicSelectSource);
+server.post('/dialog/field-refresh', onFieldRefreshDialogRequest);
+server.post('/dialog/multistep', onMultistepDialogRequest);
+server.post('/dialog/action_button_request', onActionButtonDialogRequest);
+server.post('/dialog/open_child', onOpenChildDialog);
+server.post('/field_refresh_source', onFieldRefreshSource);
+server.post('/datetime_dialog_request', onDateTimeDialogRequest);
+server.post('/datetime_dialog_submit', onDateTimeDialogSubmit);
+server.post('/slack_compatible_message_response', postSlackCompatibleMessageResponse);
+server.post('/mm_blocks_integration', postMmBlocksIntegration);
+server.post('/mm_blocks_integration_update', postMmBlocksIntegrationUpdate);
+server.post('/mm_blocks_integration_static_select', postMmBlocksIntegrationStaticSelect);
+server.post('/mm_blocks_integration_echo_query', postMmBlocksIntegrationEchoQuery);
+server.post('/mm_blocks_integration_echo_context', postMmBlocksIntegrationEchoContext);
+server.post('/send_message_to_channel', postSendMessageToChannel);
+server.post('/post_outgoing_webhook', postOutgoingWebhook);
+server.post('/send_oauth_credentials', postSendOauthCredentials);
+server.get('/start_oauth', getStartOAuth);
+server.get('/complete_oauth', getCompleteOauth);
+server.post('/post_oauth_message', postOAuthMessage);
+
+server.listen(port, (err) => {
+ if (err) {
+ console.error(err);
+ throw err;
+ }
+ console.log(`Webhook test server listening on port ${port}!`);
+});
+
+function ping(req, res) {
+ return res.json({
+ message: "I'm alive!",
+ endpoints: [
+ 'GET /',
+ 'POST /setup',
+ 'POST /message_menus',
+ 'POST /dialog_request',
+ 'POST /simple_dialog_request',
+ 'POST /user_and_channel_dialog_request',
+ 'POST /dialog_submit',
+ 'POST /boolean_dialog_request',
+ 'POST /multiselect_dialog_request',
+ 'POST /dynamic_select_dialog_request',
+ 'POST /file_upload_dialog_request',
+ 'POST /dynamic_select_source',
+ 'POST /dialog/field-refresh',
+ 'POST /dialog/multistep',
+ 'POST /dialog/action_button_request',
+ 'POST /dialog/open_child',
+ 'POST /field_refresh_source',
+ 'POST /datetime_dialog_request',
+ 'POST /datetime_dialog_submit',
+ 'POST /slack_compatible_message_response',
+ 'POST /mm_blocks_integration',
+ 'POST /mm_blocks_integration_update',
+ 'POST /mm_blocks_integration_static_select',
+ 'POST /mm_blocks_integration_echo_query',
+ 'POST /mm_blocks_integration_echo_context',
+ 'POST /send_message_to_channel',
+ 'POST /post_outgoing_webhook',
+ 'POST /send_oauth_credentials',
+ 'GET /start_oauth',
+ 'GET /complete_oauth',
+ 'POST /post_oauth_message',
+ ],
+ });
+}
+
+// Set base URLs and credential to be accessible by any endpoint
+let baseUrl;
+let webhookBaseUrl;
+let adminUsername;
+let adminPassword;
+function doSetup(req, res) {
+ baseUrl = req.body.baseUrl;
+ webhookBaseUrl = req.body.webhookBaseUrl;
+ adminUsername = req.body.adminUsername;
+ adminPassword = req.body.adminPassword;
+
+ return res.status(201).send('Successfully setup the new base URLs and credential.');
+}
+
+let client;
+let authedUser;
+function postSendOauthCredentials(req, res) {
+ const {appID, appSecret} = req.body;
+ client = new ClientOAuth2({
+ clientId: appID,
+ clientSecret: appSecret,
+ authorizationUri: `${baseUrl}/oauth/authorize`,
+ accessTokenUri: `${baseUrl}/oauth/access_token`,
+ redirectUri: `${webhookBaseUrl}/complete_oauth`,
+ });
+ return res.status(200).send('OK');
+}
+
+function getStartOAuth(req, res) {
+ return res.redirect(client.code.getUri());
+}
+
+function getCompleteOauth(req, res) {
+ client.code
+ .getToken(req.originalUrl)
+ .then((user) => {
+ authedUser = user;
+ return res.status(200).send('OK');
+ })
+ .catch((reason) => {
+ return res.status(reason.status).send(reason);
+ });
+}
+
+async function postOAuthMessage(req, res) {
+ const {channelId, message, rootId, createAt} = req.body;
+ const apiUrl = `${baseUrl}/api/v4/posts`;
+ authedUser.sign({
+ method: 'post',
+ url: apiUrl,
+ });
+ try {
+ await axios({
+ url: apiUrl,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest',
+ Authorization: 'Bearer ' + authedUser.accessToken,
+ },
+ method: 'post',
+ data: {
+ channel_id: channelId,
+ message,
+ type: '',
+ create_at: createAt,
+ root_id: rootId,
+ },
+ });
+ } catch {
+ // Do nothing
+ }
+ return res.status(200).send('OK');
+}
+
+function postSlackCompatibleMessageResponse(req, res) {
+ const {spoiler, skipSlackParsing} = req.body.context;
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({
+ ephemeral_text: spoiler,
+ skip_slack_parsing: skipSlackParsing,
+ });
+}
+
+/**
+ * Mattermost mm_blocks external actions POST the same integration envelope as legacy message buttons.
+ * @see model.PostActionIntegrationResponse
+ */
+function postMmBlocksIntegration(req, res) {
+ const userName = req.body && req.body.user_name ? req.body.user_name : 'unknown';
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.status(200).json({
+ ephemeral_text: `Playwright mm_blocks integration OK (user: ${userName}).`,
+ skip_slack_parsing: true,
+ });
+}
+
+/**
+ * Returns a PostActionIntegrationResponse update so the interactive post is edited in-place
+ * (persisted webhook post or ephemeral mm_blocks post).
+ */
+function postMmBlocksIntegrationUpdate(req, res) {
+ res.setHeader('Content-Type', 'application/json');
+ return res.status(200).json({
+ update: {
+ message: 'E2E mm_blocks post updated (message field).',
+ props: {
+ mm_blocks: [
+ {
+ type: 'text',
+ text: 'PLAYWRIGHT_MM_BLOCKS_UPDATED',
+ },
+ ],
+ },
+ },
+ skip_slack_parsing: true,
+ });
+}
+
+/** Echoes URL query parameters Mattermost merged onto the integration request (action query + block query). */
+function postMmBlocksIntegrationEchoQuery(req, res) {
+ const entries = Object.keys(req.query || {})
+ .sort()
+ .map((k) => `${k}=${String(req.query[k])}`);
+ const summary = entries.join('&');
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.status(200).json({
+ ephemeral_text: `Playwright mm_blocks query OK (${summary})`,
+ skip_slack_parsing: true,
+ });
+}
+
+/** Echoes `context.test_marker` from the Mattermost integration POST body for mm_blocks external actions. */
+function postMmBlocksIntegrationEchoContext(req, res) {
+ const ctx = (req.body && req.body.context) || {};
+ const marker = typeof ctx.test_marker === 'string' ? ctx.test_marker : JSON.stringify(ctx.test_marker ?? null);
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.status(200).json({
+ ephemeral_text: `Playwright mm_blocks context OK (test_marker: ${marker}).`,
+ skip_slack_parsing: true,
+ });
+}
+
+/** Echoes `context.selected_option` from the Mattermost integration POST for mm_blocks static_select. */
+function postMmBlocksIntegrationStaticSelect(req, res) {
+ const selected = req.body && req.body.context && req.body.context.selected_option;
+ const label = typeof selected === 'string' ? selected : JSON.stringify(selected ?? null);
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.status(200).json({
+ ephemeral_text: `Playwright mm_blocks static_select OK (selected_option: ${label}).`,
+ skip_slack_parsing: true,
+ });
+}
+
+function postMessageMenus(req, res) {
+ let responseData = {};
+ const {body} = req;
+ if (body && body.context.action === 'do_something') {
+ responseData = {
+ ephemeral_text: `Ephemeral | ${body.type} ${body.data_source} option: ${body.context.selected_option}`,
+ };
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json(responseData);
+}
+
+async function openDialog(dialog) {
+ // Callers invoke this fire-and-forget (no await/catch), so any rejection here
+ // would become an unhandled rejection and crash the whole webhook process.
+ // Guard against a missing baseUrl (set by /setup) and swallow request errors.
+ if (!baseUrl) {
+ console.error('openDialog called before /setup ran — baseUrl is not set; skipping dialog open');
+ return;
+ }
+
+ try {
+ await axios({
+ method: 'post',
+ url: `${baseUrl}/api/v4/actions/dialogs/open`,
+ data: dialog,
+ });
+ } catch (err) {
+ const status = err.response && err.response.status;
+ const body = err.response && err.response.data;
+ console.error(
+ 'openDialog request failed:',
+ status || err.code || err.message,
+ body ? JSON.stringify(body) : '',
+ );
+ }
+}
+
+function onDialogRequest(req, res) {
+ const {body} = req;
+ if (body.trigger_id) {
+ const dialog = webhookUtils.getFullDialog(body.trigger_id, webhookBaseUrl);
+ openDialog(dialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({text: 'Full dialog triggered via slash command!'});
+}
+
+function onSimpleDialogRequest(req, res) {
+ const {body} = req;
+ if (body.trigger_id) {
+ const dialog = webhookUtils.getSimpleDialog(body.trigger_id, webhookBaseUrl);
+ openDialog(dialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({text: 'Simple dialog triggered via slash command!'});
+}
+
+function onUserAndChannelDialogRequest(req, res) {
+ const {body} = req;
+ if (body.trigger_id) {
+ const dialog = webhookUtils.getUserAndChannelDialog(body.trigger_id, webhookBaseUrl);
+ openDialog(dialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({text: 'Simple dialog triggered via slash command!'});
+}
+
+function onBooleanDialogRequest(req, res) {
+ const {body} = req;
+ if (body.trigger_id) {
+ const dialog = webhookUtils.getBooleanDialog(body.trigger_id, webhookBaseUrl);
+ openDialog(dialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({text: 'Simple dialog triggered via slash command!'});
+}
+
+function onMultiSelectDialogRequest(req, res) {
+ const {body} = req;
+ if (body.trigger_id) {
+ // Check URL parameters or body for includeDefaults flag
+ const includeDefaults = req.query.includeDefaults === 'true' || req.query.includeDefaults === true;
+ const dialog = webhookUtils.getMultiSelectDialog(body.trigger_id, webhookBaseUrl, includeDefaults);
+ openDialog(dialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({text: 'Multiselect dialog triggered via slash command!'});
+}
+
+function onDynamicSelectDialogRequest(req, res) {
+ const {body} = req;
+ if (body.trigger_id) {
+ const dialog = webhookUtils.getDynamicSelectDialog(body.trigger_id, webhookBaseUrl);
+ openDialog(dialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({text: 'Dynamic select dialog triggered via slash command!'});
+}
+
+function onFileUploadDialogRequest(req, res) {
+ const {body} = req;
+ if (body.trigger_id) {
+ const dialog = webhookUtils.getFileUploadDialog(body.trigger_id, webhookBaseUrl);
+ openDialog(dialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({text: 'File upload dialog triggered via slash command!'});
+}
+
+function onDynamicSelectSource(req, res) {
+ const {body} = req;
+
+ // Simulate dynamic options based on search text
+ const searchText = (body.submission.query || '').toLowerCase();
+
+ const allOptions = [
+ {text: 'Backend Engineer', value: 'backend_eng'},
+ {text: 'Frontend Engineer', value: 'frontend_eng'},
+ {text: 'Full Stack Engineer', value: 'fullstack_eng'},
+ {text: 'DevOps Engineer', value: 'devops_eng'},
+ {text: 'QA Engineer', value: 'qa_eng'},
+ {text: 'Product Manager', value: 'product_mgr'},
+ {text: 'Engineering Manager', value: 'eng_mgr'},
+ {text: 'Senior Backend Engineer', value: 'sr_backend_eng'},
+ {text: 'Senior Frontend Engineer', value: 'sr_frontend_eng'},
+ {text: 'Principal Engineer', value: 'principal_eng'},
+ {text: 'Staff Engineer', value: 'staff_eng'},
+ {text: 'Technical Lead', value: 'tech_lead'},
+ ];
+
+ // Filter options based on search text
+ const filteredOptions = searchText
+ ? allOptions.filter(
+ (option) =>
+ option.text.toLowerCase().includes(searchText) || option.value.toLowerCase().includes(searchText),
+ )
+ : allOptions.slice(0, 6); // Limit to first 6 if no search
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({
+ items: filteredOptions,
+ });
+}
+
+function onDateTimeDialogRequest(req, res) {
+ const {body} = req;
+ if (body.trigger_id) {
+ let dialog;
+ const command = body.text ? body.text.trim() : '';
+
+ // Use focused dialog functions based on command parameter
+ switch (command) {
+ case 'basic':
+ dialog = webhookUtils.getBasicDateDialog(body.trigger_id, webhookBaseUrl);
+ break;
+ case 'mindate':
+ dialog = webhookUtils.getMinDateConstraintDialog(body.trigger_id, webhookBaseUrl);
+ break;
+ case 'interval':
+ dialog = webhookUtils.getCustomIntervalDialog(body.trigger_id, webhookBaseUrl);
+ break;
+ case 'relative':
+ dialog = webhookUtils.getRelativeDateDialog(body.trigger_id, webhookBaseUrl);
+ break;
+ case 'timezone-manual':
+ dialog = webhookUtils.getTimezoneManualDialog(body.trigger_id, webhookBaseUrl);
+ break;
+ default:
+ // Default to basic datetime dialog for backward compatibility
+ dialog = webhookUtils.getBasicDateTimeDialog(body.trigger_id, webhookBaseUrl);
+ break;
+ }
+ console.log('Opening DateTime dialog', dialog.dialog.title);
+ openDialog(dialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({text: 'DateTime dialog triggered via slash command!'});
+}
+
+function onDateTimeDialogSubmit(req, res) {
+ console.log('DateTime dialog submit handler called!');
+ const {body} = req;
+
+ res.setHeader('Content-Type', 'application/json');
+
+ // Log the submitted datetime values for debugging
+ console.log('DateTime dialog submission:', JSON.stringify(body, null, 2));
+
+ // Extract datetime values from submission
+ const submission = body.submission || {};
+ const eventDate = submission.event_date;
+ const meetingTime = submission.meeting_time;
+ const relativeDate = submission.relative_date;
+ const relativeDateTime = submission.relative_datetime;
+
+ // Create a success message with the submitted values
+ let message = 'Form submitted successfully! ';
+ if (eventDate || meetingTime || relativeDate || relativeDateTime) {
+ message += 'Submitted values: ';
+ if (eventDate) {
+ message += `Event Date: ${eventDate}, `;
+ }
+ if (meetingTime) {
+ message += `Meeting Time: ${meetingTime}, `;
+ }
+ if (relativeDate) {
+ message += `Relative Date: ${relativeDate}, `;
+ }
+ if (relativeDateTime) {
+ message += `Relative DateTime: ${relativeDateTime}, `;
+ }
+ message = message.slice(0, -2); // Remove trailing comma and space
+ }
+
+ // Send success response that will appear as a post in the channel
+ sendSysadminResponse(message, body.channel_id);
+ return res.json({text: message});
+}
+
+function onDialogSubmit(req, res) {
+ const {body} = req;
+
+ res.setHeader('Content-Type', 'application/json');
+
+ let message;
+ if (body.cancelled) {
+ message = 'Dialog cancelled';
+ console.log('[WEBHOOK] Dialog cancelled');
+ sendSysadminResponse(message, body.channel_id);
+ return res.json({text: message});
+ }
+
+ // Check if this is a multistep submission
+ if (body.callback_id === 'multistep_callback') {
+ const currentState = body.state || '';
+
+ // Determine next step based on current state
+ if (currentState === 'step1') {
+ // Move to step 2
+ const nextForm = webhookUtils.getMultistepStep2Dialog(null, webhookBaseUrl);
+ return res.json({
+ type: 'form',
+ form: nextForm,
+ });
+ } else if (currentState === 'step2') {
+ // Move to step 3
+ const nextForm = webhookUtils.getMultistepStep3Dialog(null, webhookBaseUrl);
+ return res.json({
+ type: 'form',
+ form: nextForm,
+ });
+ }
+
+ // Final step - complete the multistep
+ const submission = body.submission || {};
+ message = `Multistep completed successfully! Final step values: ${JSON.stringify(submission, null, 2)}`;
+ sendSysadminResponse(message, body.channel_id);
+ return res.json({text: message});
+ }
+
+ // Check if this is a field refresh dialog submission
+ if (body.callback_id === 'field_refresh_callback') {
+ const submission = body.submission || {};
+ message = `Field refresh dialog submitted successfully! Values: ${JSON.stringify(submission, null, 2)}`;
+ sendSysadminResponse(message, body.channel_id);
+ return res.json({text: message});
+ }
+
+ // Regular dialog submission
+ // Format submission data for the channel message
+ const sanitize = (str) => String(str).replace(/[<>&"']/g, (ch) => `${ch.charCodeAt(0)};`);
+ const submissionData = Object.entries(body.submission || {})
+ .map(([key, value]) => `**${sanitize(key)}**: ${sanitize(value)}`)
+ .join('\n');
+
+ message = `Dialog submitted successfully!\n\n**Submission Data:**\n${submissionData}`;
+
+ sendSysadminResponse(message, body.channel_id);
+ return res.json({text: message});
+}
+
+/**
+ * @route "POST /send_message_to_channel?type={messageType}&channel_id={channelId}"
+ * @query type - message type of empty string for regular message if not provided (default), "system_message", etc
+ * @query channel_id - channel where to send the message
+ */
+function postSendMessageToChannel(req, res) {
+ const channelId = req.query.channel_id;
+ const response = {
+ response_type: 'in_channel',
+ text: 'Extra response 2',
+ channel_id: channelId,
+ extra_responses: [
+ {
+ response_type: 'in_channel',
+ text: 'Hello World',
+ channel_id: channelId,
+ },
+ ],
+ };
+
+ if (req.query.type) {
+ response.type = req.query.type;
+ }
+
+ res.json(response);
+}
+
+// Convenient way to send response in a channel by using sysadmin account
+function sendSysadminResponse(message, channelId) {
+ postMessageAs({
+ sender: {
+ username: adminUsername,
+ password: adminPassword,
+ },
+ message,
+ channelId,
+ baseUrl,
+ });
+}
+
+const responseTypes = ['in_channel', 'comment'];
+
+function getWebhookResponse(body, {responseType, username, iconUrl}) {
+ const payload = Object.entries(body)
+ .map(([key, value]) => `- ${key}: "${value}"`)
+ .join('\n');
+
+ return `
+\`\`\`
+#### Outgoing Webhook Payload
+${payload}
+#### Webhook override to Mattermost instance
+- response_type: "${responseType}"
+- type: ""
+- username: "${username}"
+- icon_url: "${iconUrl}"
+\`\`\`
+`;
+}
+
+/**
+ * @route "POST /post_outgoing_webhook?override_username={username}&override_icon_url={iconUrl}&response_type={comment}"
+ * @query override_username - the user name that overrides the user name defined by the outgoing webhook
+ * @query override_icon_url - the user icon url that overrides the user icon url defined by the outgoing webhook
+ * @query response_type - "in_channel" (default) or "comment"
+ */
+function postOutgoingWebhook(req, res) {
+ const {body, query} = req;
+ if (!body) {
+ res.status(404).send({error: 'Invalid data'});
+ }
+
+ const responseType = query.response_type || responseTypes[0];
+ const username = query.override_username || '';
+ const iconUrl = query.override_icon_url || '';
+
+ const response = {
+ text: getWebhookResponse(body, {responseType, username, iconUrl}),
+ username,
+ icon_url: iconUrl,
+ type: '',
+ response_type: responseType,
+ };
+ res.status(200).send(response);
+}
+
+function onFieldRefreshDialogRequest(req, res) {
+ const {body} = req;
+ if (body.trigger_id) {
+ const dialog = webhookUtils.getFieldRefreshDialog(body.trigger_id, webhookBaseUrl);
+ openDialog(dialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({text: 'Field refresh dialog triggered via slash command!'});
+}
+
+function onMultistepDialogRequest(req, res) {
+ const {body} = req;
+ if (body.trigger_id) {
+ const dialog = webhookUtils.getMultistepStep1Dialog(body.trigger_id, webhookBaseUrl);
+ openDialog(dialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({text: 'Multistep dialog triggered via slash command!'});
+}
+
+function onActionButtonDialogRequest(req, res) {
+ const {body} = req;
+ if (body.trigger_id) {
+ const dialog = webhookUtils.getActionButtonParentDialog(body.trigger_id, webhookBaseUrl);
+ openDialog(dialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({text: 'Action button dialog triggered!'});
+}
+
+async function onOpenChildDialog(req, res) {
+ const {body} = req;
+
+ // context.source identifies which action button on the parent dialog was
+ // pressed; it is forwarded by the server in the PostActionIntegrationRequest.
+ const source = (body.context && body.context.source) || 'Unknown';
+ console.log('onOpenChildDialog called with trigger_id:', body.trigger_id, 'source:', source);
+ if (body.trigger_id) {
+ const childDialog = webhookUtils.getActionButtonChildDialog(body.trigger_id, webhookBaseUrl, source);
+
+ // Await the dialog open before responding. The server's /execute call
+ // (DoActionRequest) waits for this response, so awaiting here ensures the
+ // child's WS open_dialog event is published before the browser's
+ // executeDialogAction promise resolves — removing the render race in tests.
+ await openDialog(childDialog);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.json({});
+}
+
+function onFieldRefreshSource(req, res) {
+ const {body} = req;
+ const submission = body.submission || {};
+ const projectType = submission.project_type;
+ const projectName = submission.project_name || '';
+
+ res.setHeader('Content-Type', 'application/json');
+
+ // Return updated form based on project type selection
+ const elements = [
+ {
+ display_name: 'Project Name',
+ name: 'project_name',
+ type: 'text',
+ placeholder: 'Enter project name',
+ default: projectName,
+ optional: false,
+ },
+ {
+ display_name: 'Project Type',
+ name: 'project_type',
+ type: 'select',
+ refresh: true,
+ placeholder: 'Select project type...',
+ default: projectType,
+ options: [
+ {text: 'Web Application', value: 'web'},
+ {text: 'Mobile App', value: 'mobile'},
+ {text: 'API Service', value: 'api'},
+ ],
+ },
+ ];
+
+ // Add different fields based on project type
+ if (projectType === 'web') {
+ elements.push({
+ display_name: 'Framework',
+ name: 'framework',
+ type: 'select',
+ placeholder: 'Select framework...',
+ options: [
+ {text: 'React', value: 'react'},
+ {text: 'Vue', value: 'vue'},
+ {text: 'Angular', value: 'angular'},
+ ],
+ });
+ } else if (projectType === 'mobile') {
+ elements.push({
+ display_name: 'Platform',
+ name: 'platform',
+ type: 'select',
+ placeholder: 'Select platform...',
+ options: [
+ {text: 'iOS', value: 'ios'},
+ {text: 'Android', value: 'android'},
+ {text: 'React Native', value: 'react-native'},
+ ],
+ });
+ } else if (projectType === 'api') {
+ elements.push({
+ display_name: 'Language',
+ name: 'language',
+ type: 'select',
+ placeholder: 'Select language...',
+ options: [
+ {text: 'Go', value: 'go'},
+ {text: 'Node.js', value: 'nodejs'},
+ {text: 'Python', value: 'python'},
+ ],
+ });
+ }
+
+ return res.json({
+ type: 'form',
+ form: {
+ title: 'Field Refresh Demo',
+ introduction_text: 'Enter project name then select type to see different fields',
+ submit_label: 'Submit',
+ elements,
+ },
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/containers/azurite_container.ts b/e2e-tests/playwright/lib/src/containers/azurite_container.ts
new file mode 100644
index 000000000000..3f88e0b0cb1e
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/azurite_container.ts
@@ -0,0 +1,37 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {GenericContainer, Wait} from 'testcontainers';
+import type {StartedNetwork, StartedTestContainer} from 'testcontainers';
+
+import {AZURITE_ALIAS, AZURITE_BLOB_PORT, TESTCONTAINERS_LABELS} from './constants';
+import {AZURITE_IMAGE} from './default_images';
+import {startWithRetry} from './retry';
+
+import {testConfig} from '@/test_config';
+
+// An alternative to Minio for blob storage.
+export async function startAzuriteContainer(network: StartedNetwork): Promise {
+ return startWithRetry('azurite', async () => {
+ let builder = new GenericContainer(AZURITE_IMAGE)
+ .withCommand([
+ 'azurite-blob',
+ '--blobHost',
+ '0.0.0.0',
+ '--blobPort',
+ String(AZURITE_BLOB_PORT),
+ '--skipApiVersionCheck',
+ ])
+ .withExposedPorts(AZURITE_BLOB_PORT)
+ .withNetwork(network)
+ .withNetworkAliases(AZURITE_ALIAS)
+ .withLabels(TESTCONTAINERS_LABELS)
+ .withWaitStrategy(Wait.forListeningPorts());
+
+ if (testConfig.testcontainersReuse) {
+ builder = builder.withReuse();
+ }
+
+ return builder.start();
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/containers/constants.ts b/e2e-tests/playwright/lib/src/containers/constants.ts
new file mode 100644
index 000000000000..873bbe98ecea
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/constants.ts
@@ -0,0 +1,68 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+// Single source of truth for the fixed values every container/helper needs to agree on.
+// Kept separate from test_config.ts because these are not overridable — they're either
+// Testcontainers network aliases (only meaningful inside the Testcontainers network) or
+// fixed test-only credentials for a throwaway local service.
+
+export const POSTGRES_ALIAS = 'postgres';
+export const POSTGRES_PORT = 5432;
+export const POSTGRES_DB = 'mattermost_test';
+export const POSTGRES_USER = 'mmuser';
+export const POSTGRES_PASSWORD = 'mostest';
+
+export const INBUCKET_ALIAS = 'inbucket';
+export const INBUCKET_WEB_PORT = 9001;
+export const INBUCKET_SMTP_PORT = 10025;
+export const INBUCKET_POP3_PORT = 10110;
+
+export const MATTERMOST_ALIAS = 'server';
+export const MATTERMOST_PORT = 8065;
+
+// Interactive-message/dialog callback sidecar shared with Cypress. Always started, like
+// postgres/inbucket — not gated behind testcontainersServices.
+export const WEBHOOK_ALIAS = 'webhook';
+export const WEBHOOK_PORT = 3000;
+
+export const OPENLDAP_ALIAS = 'openldap';
+export const OPENLDAP_PORT = 389;
+export const OPENLDAP_ADMIN_DN = 'cn=admin,dc=mm,dc=test,dc=com';
+export const OPENLDAP_ADMIN_PASSWORD = 'mostest';
+export const OPENLDAP_BASE_DN = 'dc=mm,dc=test,dc=com';
+
+export const KEYCLOAK_ALIAS = 'keycloak';
+export const KEYCLOAK_PORT = 8080;
+export const KEYCLOAK_REALM = 'mattermost';
+export const KEYCLOAK_ADMIN_USER = 'admin';
+export const KEYCLOAK_ADMIN_PASSWORD = 'admin';
+
+export const ELASTICSEARCH_ALIAS = 'elasticsearch';
+export const ELASTICSEARCH_PORT = 9200;
+
+export const MINIO_ALIAS = 'minio';
+export const MINIO_PORT = 9000;
+export const MINIO_ACCESS_KEY = 'minioaccesskey';
+export const MINIO_SECRET_KEY = 'miniosecretkey';
+export const MINIO_BUCKET = 'mattermost-test';
+
+// Alternative to Minio for blob storage.
+export const AZURITE_ALIAS = 'azurite';
+export const AZURITE_BLOB_PORT = 10000;
+// Azurite's well-known default emulator account — published by Microsoft's own docs, not a secret.
+export const AZURITE_ACCOUNT_NAME = 'devstoreaccount1';
+export const AZURITE_ACCOUNT_KEY =
+ 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==';
+export const AZURITE_CONTAINER = 'mattermost-test';
+
+// Alternative to Elasticsearch for search.
+export const OPENSEARCH_ALIAS = 'opensearch';
+export const OPENSEARCH_PORT = 9201;
+export const OPENSEARCH_ADMIN_PASSWORD = 'Test@dmin_123';
+
+// Applied to every container this module starts, so `npm run testcontainers:down` can find and
+// remove them from a fresh process — the in-memory `started` state in stack.ts only exists in
+// the process that created it.
+export const TESTCONTAINERS_LABEL_KEY = 'mm-playwright-testcontainers';
+export const TESTCONTAINERS_LABEL_VALUE = 'true';
+export const TESTCONTAINERS_LABELS = {[TESTCONTAINERS_LABEL_KEY]: TESTCONTAINERS_LABEL_VALUE};
diff --git a/e2e-tests/playwright/lib/src/containers/default_images.ts b/e2e-tests/playwright/lib/src/containers/default_images.ts
new file mode 100644
index 000000000000..f47508c37ff2
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/default_images.ts
@@ -0,0 +1,18 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+// Single place to check and bump every image.
+// The Mattermost server's default is overridable via the SERVER_IMAGE env var (testConfig.serverImage).
+export const MATTERMOST_SERVER_IMAGE = 'mattermostdevelopment/mattermost-enterprise-edition:master';
+export const POSTGRES_IMAGE = 'postgres:14';
+export const INBUCKET_IMAGE = 'inbucket/inbucket:3.1.1';
+export const OPENLDAP_IMAGE = 'osixia/openldap:1.4.0';
+export const KEYCLOAK_IMAGE = 'quay.io/keycloak/keycloak:23.0.7';
+export const MINIO_IMAGE = 'minio/minio:RELEASE.2024-06-22T05-26-45Z';
+export const AZURITE_IMAGE = 'mcr.microsoft.com/azure-storage/azurite:3.34.0';
+// Built from a Dockerfile on top of docker.elastic.co/elasticsearch/elasticsearch, rather than
+// pulled as a fixed image — so only the version is fixed here.
+export const ELASTICSEARCH_VERSION = '9.0.0';
+// Built from a Dockerfile on top of opensearchproject/opensearch, rather than pulled as a fixed
+// image — so only the version is fixed here.
+export const OPENSEARCH_VERSION = '3.0.0';
diff --git a/e2e-tests/playwright/lib/src/containers/elasticsearch_container.ts b/e2e-tests/playwright/lib/src/containers/elasticsearch_container.ts
new file mode 100644
index 000000000000..1242169163a6
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/elasticsearch_container.ts
@@ -0,0 +1,48 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {GenericContainer, Wait} from 'testcontainers';
+import type {StartedNetwork, StartedTestContainer} from 'testcontainers';
+
+import {ELASTICSEARCH_ALIAS, ELASTICSEARCH_PORT, TESTCONTAINERS_LABELS} from './constants';
+import {ELASTICSEARCH_VERSION as DEFAULT_ELASTICSEARCH_VERSION} from './default_images';
+import {containerAssetPath} from './paths';
+import {startWithRetry} from './retry';
+
+import {testConfig} from '@/test_config';
+
+// Built from a vendored Dockerfile, not the generic @testcontainers/elasticsearch wrapper — it
+// installs the CJK analysis plugins (analysis-icu/nori/kuromoji/smartcn) real search tests rely on.
+export async function startElasticsearchContainer(network: StartedNetwork): Promise {
+ return startWithRetry('elasticsearch', async () => {
+ // deleteOnExit: false — otherwise the built image bakes in this session's Ryuk id, so Ryuk
+ // reaps the "reused" container the moment this session ends, defeating withReuse() below.
+ const image = await GenericContainer.fromDockerfile(containerAssetPath(), 'Dockerfile.elasticsearch')
+ .withBuildArgs({
+ ELASTICSEARCH_VERSION: process.env.ELASTICSEARCH_VERSION || DEFAULT_ELASTICSEARCH_VERSION,
+ })
+ .build(undefined, {deleteOnExit: false});
+
+ let builder = image
+ .withExposedPorts(ELASTICSEARCH_PORT)
+ .withNetwork(network)
+ .withNetworkAliases(ELASTICSEARCH_ALIAS)
+ .withLabels(TESTCONTAINERS_LABELS)
+ .withEnvironment({
+ 'http.host': '0.0.0.0',
+ 'http.port': String(ELASTICSEARCH_PORT),
+ 'xpack.security.enabled': 'false',
+ 'action.destructive_requires_name': 'false',
+ 'transport.host': '127.0.0.1',
+ ES_JAVA_OPTS: '-Xms512m -Xmx512m',
+ })
+ .withStartupTimeout(3 * 60_000)
+ .withWaitStrategy(Wait.forHttp('/_cluster/health', ELASTICSEARCH_PORT).forStatusCode(200));
+
+ if (testConfig.testcontainersReuse) {
+ builder = builder.withReuse();
+ }
+
+ return builder.start();
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/containers/env_baseline.ts b/e2e-tests/playwright/lib/src/containers/env_baseline.ts
new file mode 100644
index 000000000000..e288d8f85631
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/env_baseline.ts
@@ -0,0 +1,27 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+// Test-oriented MM_* config the Mattermost server container starts with by default,
+// merged under testConfig.serverEnv (MM_ENV) so callers can still override any of it.
+export const SERVER_ENV_BASELINE: Record = {
+ MM_SERVICEENVIRONMENT: 'test',
+ MM_CLUSTERSETTINGS_READONLYCONFIG: 'false',
+ MM_CONNECTEDWORKSPACESSETTINGS_ENABLEREMOTECLUSTERSERVICE: 'true',
+ MM_CONNECTEDWORKSPACESSETTINGS_ENABLESHAREDWORKSPACES: 'true',
+ MM_LOGSETTINGS_CONSOLELEVEL: 'DEBUG',
+ MM_LOGSETTINGS_ENABLEDIAGNOSTICS: 'false',
+ MM_PLUGINSETTINGS_ENABLEUPLOADS: 'true',
+ MM_SERVICESETTINGS_ALLOWCORSFROM: '*',
+ MM_SERVICESETTINGS_ALLOWEDUNTRUSTEDINTERNALCONNECTIONS: 'keycloak elasticsearch opensearch minio azurite webhook',
+ MM_SERVICESETTINGS_ENABLELOCALMODE: 'true',
+ MM_SERVICESETTINGS_ENABLESECURITYFIXALERT: 'false',
+ MM_SERVICESETTINGS_ENABLETESTING: 'true',
+ // Feature flags this test suite needs on, off by default in the server
+ MM_FEATUREFLAGS_ATTRIBUTEVALUEMASKING: 'true',
+ MM_FEATUREFLAGS_ENABLEREMOTECLUSTERSERVICE: 'true',
+ MM_FEATUREFLAGS_MOVETHREADSENABLED: 'true',
+ MM_FEATUREFLAGS_PERMISSIONPOLICIES: 'true',
+ MM_FEATUREFLAGS_PROPERTYFIELDRANK: 'true',
+ MM_FEATUREFLAGS_TEAMMEMBERSHIPACCESSCONTROL: 'true',
+ MM_FEATUREFLAGS_WYSIWYGEDITOR: 'true',
+};
diff --git a/e2e-tests/playwright/lib/src/containers/inbucket_container.ts b/e2e-tests/playwright/lib/src/containers/inbucket_container.ts
new file mode 100644
index 000000000000..8a77344e293b
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/inbucket_container.ts
@@ -0,0 +1,39 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {GenericContainer, Wait} from 'testcontainers';
+import type {StartedNetwork, StartedTestContainer} from 'testcontainers';
+
+import {
+ INBUCKET_ALIAS,
+ INBUCKET_POP3_PORT,
+ INBUCKET_SMTP_PORT,
+ INBUCKET_WEB_PORT,
+ TESTCONTAINERS_LABELS,
+} from './constants';
+import {INBUCKET_IMAGE} from './default_images';
+import {startWithRetry} from './retry';
+
+import {testConfig} from '@/test_config';
+
+export async function startInbucketContainer(network: StartedNetwork): Promise {
+ return startWithRetry('inbucket', async () => {
+ let builder = new GenericContainer(INBUCKET_IMAGE)
+ .withExposedPorts(INBUCKET_WEB_PORT, INBUCKET_SMTP_PORT, INBUCKET_POP3_PORT)
+ .withNetwork(network)
+ .withNetworkAliases(INBUCKET_ALIAS)
+ .withLabels(TESTCONTAINERS_LABELS)
+ .withEnvironment({
+ INBUCKET_WEB_ADDR: `0.0.0.0:${INBUCKET_WEB_PORT}`,
+ INBUCKET_POP3_ADDR: `0.0.0.0:${INBUCKET_POP3_PORT}`,
+ INBUCKET_SMTP_ADDR: `0.0.0.0:${INBUCKET_SMTP_PORT}`,
+ })
+ .withWaitStrategy(Wait.forListeningPorts());
+
+ if (testConfig.testcontainersReuse) {
+ builder = builder.withReuse();
+ }
+
+ return builder.start();
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/containers/index.ts b/e2e-tests/playwright/lib/src/containers/index.ts
new file mode 100644
index 000000000000..a99fac84bbf5
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/index.ts
@@ -0,0 +1,4 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+export {startStack, stopStack} from './stack';
diff --git a/e2e-tests/playwright/lib/src/containers/keycloak_container.ts b/e2e-tests/playwright/lib/src/containers/keycloak_container.ts
new file mode 100644
index 000000000000..7165b5575637
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/keycloak_container.ts
@@ -0,0 +1,51 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {GenericContainer, Wait} from 'testcontainers';
+import type {StartedNetwork, StartedTestContainer} from 'testcontainers';
+
+import {
+ KEYCLOAK_ADMIN_PASSWORD,
+ KEYCLOAK_ADMIN_USER,
+ KEYCLOAK_ALIAS,
+ KEYCLOAK_PORT,
+ TESTCONTAINERS_LABELS,
+} from './constants';
+import {KEYCLOAK_IMAGE} from './default_images';
+import {containerAssetPath} from './paths';
+import {startWithRetry} from './retry';
+
+import {testConfig} from '@/test_config';
+
+export async function startKeycloakContainer(network: StartedNetwork): Promise {
+ return startWithRetry('keycloak', async () => {
+ let builder = new GenericContainer(KEYCLOAK_IMAGE)
+ .withEntrypoint(['/opt/keycloak/bin/kc.sh'])
+ .withCommand(['start', '--import-realm'])
+ .withExposedPorts(KEYCLOAK_PORT)
+ .withNetwork(network)
+ .withNetworkAliases(KEYCLOAK_ALIAS)
+ .withLabels(TESTCONTAINERS_LABELS)
+ .withEnvironment({
+ KEYCLOAK_ADMIN: KEYCLOAK_ADMIN_USER,
+ KEYCLOAK_ADMIN_PASSWORD,
+ KC_HOSTNAME_STRICT: 'false',
+ KC_HOSTNAME_STRICT_HTTPS: 'false',
+ KC_HTTP_ENABLED: 'true',
+ })
+ .withCopyFilesToContainer([
+ {
+ source: containerAssetPath('keycloak-realm-export.json'),
+ target: '/opt/keycloak/data/import/realm-export.json',
+ },
+ ])
+ .withStartupTimeout(3 * 60_000)
+ .withWaitStrategy(Wait.forHttp('/realms/master', KEYCLOAK_PORT).forStatusCode(200));
+
+ if (testConfig.testcontainersReuse) {
+ builder = builder.withReuse();
+ }
+
+ return builder.start();
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/containers/log.ts b/e2e-tests/playwright/lib/src/containers/log.ts
new file mode 100644
index 000000000000..45b2ed4ad7b9
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/log.ts
@@ -0,0 +1,21 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import chalk from 'chalk';
+
+const PREFIX = chalk.cyan('[testcontainers]');
+
+export function logTestcontainers(message: string): void {
+ // eslint-disable-next-line no-console
+ console.log(`${PREFIX} ${message}`);
+}
+
+export function warnTestcontainers(message: string): void {
+ // eslint-disable-next-line no-console
+ console.warn(`${PREFIX} ${message}`);
+}
+
+export function errorTestcontainers(message: string): void {
+ // eslint-disable-next-line no-console
+ console.error(`${PREFIX} ${message}`);
+}
diff --git a/e2e-tests/playwright/lib/src/containers/mattermost_container.ts b/e2e-tests/playwright/lib/src/containers/mattermost_container.ts
new file mode 100644
index 000000000000..18fbadf2f26f
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/mattermost_container.ts
@@ -0,0 +1,100 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {GenericContainer, Wait} from 'testcontainers';
+import type {StartedTestContainer} from 'testcontainers';
+
+import {
+ INBUCKET_ALIAS,
+ INBUCKET_SMTP_PORT,
+ MATTERMOST_ALIAS,
+ MATTERMOST_PORT,
+ POSTGRES_ALIAS,
+ POSTGRES_DB,
+ POSTGRES_PASSWORD,
+ POSTGRES_PORT,
+ POSTGRES_USER,
+ TESTCONTAINERS_LABELS,
+} from './constants';
+import {SERVER_ENV_BASELINE} from './env_baseline';
+import {startWithRetry} from './retry';
+
+import {testConfig} from '@/test_config';
+
+// Env this container computes itself from the stack Testcontainers just built. Must win over any
+// stray testConfig.serverEnv (MM_ENV) entry and over testConfig.bootEnvOverrides (passed in as
+// `extraEnv` by restartMattermostContainer()), or a stray key collision there could break the
+// server's own connectivity. Deliberately does NOT know about any additional service (LDAP/
+// Keycloak/Elasticsearch/OpenSearch/Minio/Azurite) — those are each spec's own responsibility via
+// pw.ensure(), which points the already-running server at them through patchConfig.
+//
+// MM_LICENSE (if set) is passed straight through: the server reads it directly at startup
+// (platform.LoadLicense), so it boots already licensed instead of needing an authenticated upload
+// call after the fact.
+function structuralEnv(): Record {
+ return {
+ MM_SQLSETTINGS_DRIVERNAME: 'postgres',
+ MM_SQLSETTINGS_DATASOURCE: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_ALIAS}:${POSTGRES_PORT}/${POSTGRES_DB}?sslmode=disable&connect_timeout=10&binary_parameters=yes`,
+ MM_EMAILSETTINGS_SMTPSERVER: INBUCKET_ALIAS,
+ MM_EMAILSETTINGS_SMTPPORT: String(INBUCKET_SMTP_PORT),
+ ...(process.env.MM_LICENSE ? {MM_LICENSE: process.env.MM_LICENSE} : {}),
+ // Overrides (not merges) SERVER_ENV_BASELINE's own value for this same key — appends the
+ // network's gateway IP so the SSRF guard also allows fetching from file_server.ts's mock
+ // file server, reachable at that address (see test_config.ts). Only known once the
+ // network is up (testConfig.testcontainersNetworkGatewayIp is set by stack.ts's
+ // startStack() before this container ever starts), so falls back to the baseline's own
+ // value verbatim on the off chance this ever runs without it.
+ MM_SERVICESETTINGS_ALLOWEDUNTRUSTEDINTERNALCONNECTIONS: testConfig.testcontainersNetworkGatewayIp
+ ? `${SERVER_ENV_BASELINE.MM_SERVICESETTINGS_ALLOWEDUNTRUSTEDINTERNALCONNECTIONS} ${testConfig.testcontainersNetworkGatewayIp}`
+ : SERVER_ENV_BASELINE.MM_SERVICESETTINGS_ALLOWEDUNTRUSTEDINTERNALCONNECTIONS,
+ };
+}
+
+// Readiness requires both the /api/v4/system/ping health check AND the permissions-migration job
+// scheduler's "All migrations are complete." log line (scheduler.go, jobs/migrations package).
+// Ping alone isn't enough: MigrationKeyAdvancedPermissionsPhase2 runs as an async job whose
+// scheduler deliberately delays its first tick 60s after startup — a real window a spec's very
+// first API call can otherwise land inside, tripping IsPhase2MigrationCompleted() gates with
+// "required migrations have not yet completed" (confirmed in practice: a permissions-page spec
+// hit exactly this, with the "Edit Scheme" link stuck disabled, when this wait was dropped).
+// Requires MM_LOGSETTINGS_CONSOLELEVEL=DEBUG (env_baseline.ts) since the scheduler logs that line
+// at Debug. Only paid on a genuinely fresh boot — a reused/adopted stack skips this entirely.
+//
+// Joins the network by name (withNetworkMode) rather than a StartedNetwork object: also called
+// from restartMattermostContainer(), which runs in a worker process that never holds the actual
+// StartedNetwork handle — only the network's name (threaded through testConfig) is available
+// there.
+export async function startMattermostContainer(
+ networkName: string,
+ extraEnv: Record = {},
+): Promise {
+ const env: Record = {
+ ...SERVER_ENV_BASELINE,
+ ...testConfig.serverEnv,
+ ...extraEnv,
+ ...structuralEnv(),
+ };
+
+ return startWithRetry('server', async () => {
+ let builder = new GenericContainer(testConfig.serverImage)
+ .withPlatform('linux/amd64') // The published server images are amd64-only.
+ .withNetworkMode(networkName)
+ .withNetworkAliases(MATTERMOST_ALIAS)
+ .withLabels(TESTCONTAINERS_LABELS)
+ .withExposedPorts(MATTERMOST_PORT)
+ .withEnvironment(env)
+ .withStartupTimeout(5 * 60_000)
+ .withWaitStrategy(
+ Wait.forAll([
+ Wait.forHttp('/api/v4/system/ping', MATTERMOST_PORT).forStatusCode(200),
+ Wait.forLogMessage(/All migrations are complete\./, 1),
+ ]),
+ );
+
+ if (testConfig.testcontainersReuse) {
+ builder = builder.withReuse();
+ }
+
+ return builder.start();
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/containers/minio_container.ts b/e2e-tests/playwright/lib/src/containers/minio_container.ts
new file mode 100644
index 000000000000..3f4585e00d97
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/minio_container.ts
@@ -0,0 +1,33 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {GenericContainer, Wait} from 'testcontainers';
+import type {StartedNetwork, StartedTestContainer} from 'testcontainers';
+
+import {MINIO_ACCESS_KEY, MINIO_ALIAS, MINIO_PORT, MINIO_SECRET_KEY, TESTCONTAINERS_LABELS} from './constants';
+import {MINIO_IMAGE} from './default_images';
+import {startWithRetry} from './retry';
+
+import {testConfig} from '@/test_config';
+
+export async function startMinioContainer(network: StartedNetwork): Promise {
+ return startWithRetry('minio', async () => {
+ let builder = new GenericContainer(MINIO_IMAGE)
+ .withCommand(['server', '/data', '--console-address', ':9002'])
+ .withExposedPorts(MINIO_PORT)
+ .withNetwork(network)
+ .withNetworkAliases(MINIO_ALIAS)
+ .withLabels(TESTCONTAINERS_LABELS)
+ .withEnvironment({
+ MINIO_ROOT_USER: MINIO_ACCESS_KEY,
+ MINIO_ROOT_PASSWORD: MINIO_SECRET_KEY,
+ })
+ .withWaitStrategy(Wait.forHttp('/minio/health/live', MINIO_PORT).forStatusCode(200));
+
+ if (testConfig.testcontainersReuse) {
+ builder = builder.withReuse();
+ }
+
+ return builder.start();
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/containers/mmctl_container.ts b/e2e-tests/playwright/lib/src/containers/mmctl_container.ts
new file mode 100644
index 000000000000..1226031b8b71
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/mmctl_container.ts
@@ -0,0 +1,120 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {execFile} from 'node:child_process';
+import {promisify} from 'node:util';
+
+import {GenericContainer} from 'testcontainers';
+import type {WaitStrategy} from 'testcontainers';
+
+import {MATTERMOST_ALIAS, MATTERMOST_PORT, TESTCONTAINERS_LABELS} from './constants';
+
+import {testConfig} from '@/test_config';
+
+const execFileAsync = promisify(execFile);
+
+const MMCTL_ENTRYPOINT = '/mattermost/bin/mmctl';
+const MMCTL_CONFIG_DIR = '/tmp/mmctl-xdg';
+const MMCTL_CREDENTIALS_NAME = 'e2e';
+
+export type MmctlResult = {
+ exitCode: number;
+ output: string;
+};
+
+// Matches the Credentials/CredentialsList shape mmctl reads (server/cmd/mmctl/commands/auth_utils.go).
+// Written directly instead of via `mmctl auth login`, since that needs a password file and this
+// image has no shell to create one. AuthMethod "T" treats authToken as a plain bearer token, which
+// mmctl reads into Client4.AuthToken.
+function buildCredentialsFileContent(username: string, authToken: string): string {
+ const credentialsList = {
+ [MMCTL_CREDENTIALS_NAME]: {
+ name: MMCTL_CREDENTIALS_NAME,
+ username,
+ authToken,
+ authMethod: 'T',
+ instanceUrl: `http://${MATTERMOST_ALIAS}:${MATTERMOST_PORT}`,
+ active: true,
+ },
+ };
+ return JSON.stringify(credentialsList);
+}
+
+async function streamToString(stream: NodeJS.ReadableStream): Promise {
+ const chunks: Buffer[] = [];
+ for await (const chunk of stream) {
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
+ }
+ return Buffer.concat(chunks).toString('utf-8');
+}
+
+// Wait.forOneShotStartup() throws on any non-zero exit code, but a non-zero exit from mmctl is a
+// meaningful result to inspect, not a startup failure. This strategy ignores the exit code;
+// completion is awaited afterward via `docker wait`.
+class NoOpWaitStrategy implements WaitStrategy {
+ private startupTimeoutMs = 0;
+
+ async waitUntilReady(): Promise {
+ // No-op — completion is awaited by the caller via `docker wait`.
+ }
+
+ withStartupTimeout(startupTimeoutMs: number): this {
+ this.startupTimeoutMs = startupTimeoutMs;
+ return this;
+ }
+
+ isStartupTimeoutSet(): boolean {
+ return true;
+ }
+
+ getStartupTimeout(): number {
+ return this.startupTimeoutMs;
+ }
+}
+
+async function waitForExitCode(containerId: string): Promise {
+ // Bounded so a hung mmctl command can't stall cleanup indefinitely.
+ const {stdout} = await execFileAsync('docker', ['wait', containerId], {timeout: 60_000});
+ return parseInt(stdout.trim(), 10);
+}
+
+/**
+ * Runs a single mmctl command in its own throwaway container built from the server image, acting
+ * as a real remote client rather than the `--local` unix-socket mode used for the server's healthcheck.
+ *
+ * Joins the network by name (withNetworkMode), not via getNetwork() — this runs in the Playwright
+ * worker process, a different OS process from the one that created the network, so getNetwork()'s
+ * in-process cache would create a second, unrelated network instead of finding the real one.
+ */
+export async function runMmctl(args: string[], username: string, authToken: string): Promise {
+ if (!testConfig.testcontainersNetworkName) {
+ throw new Error(
+ 'No Testcontainers network name available (PW_TESTCONTAINERS_NETWORK_NAME) — is PW_USE_TESTCONTAINERS=true?',
+ );
+ }
+
+ const container = await new GenericContainer(testConfig.serverImage)
+ .withPlatform('linux/amd64') // The published server images are amd64-only.
+ .withNetworkMode(testConfig.testcontainersNetworkName)
+ .withLabels(TESTCONTAINERS_LABELS)
+ .withEnvironment({XDG_CONFIG_HOME: MMCTL_CONFIG_DIR})
+ .withCopyContentToContainer([
+ {
+ content: buildCredentialsFileContent(username, authToken),
+ target: `${MMCTL_CONFIG_DIR}/mmctl/config`,
+ },
+ ])
+ .withEntrypoint([MMCTL_ENTRYPOINT])
+ .withCommand(args)
+ .withWaitStrategy(new NoOpWaitStrategy())
+ .withStartupTimeout(60_000)
+ .start();
+
+ try {
+ const exitCode = await waitForExitCode(container.getId());
+ const output = await streamToString(await container.logs());
+ return {exitCode, output};
+ } finally {
+ await container.stop({remove: true});
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/containers/network.ts b/e2e-tests/playwright/lib/src/containers/network.ts
new file mode 100644
index 000000000000..faaebdae75d9
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/network.ts
@@ -0,0 +1,73 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {execFile} from 'node:child_process';
+import {promisify} from 'node:util';
+
+import {Network} from 'testcontainers';
+import type {StartedNetwork} from 'testcontainers';
+
+import {logTestcontainers, warnTestcontainers} from './log';
+
+const execFileAsync = promisify(execFile);
+
+// One bridge network per Playwright invocation, shared by every container it starts. When
+// Playwright itself runs inside a container, that container also joins this network so it can
+// reach everything by alias instead of a mapped port.
+let startedNetwork: StartedNetwork | undefined;
+
+export async function getNetwork(): Promise {
+ if (!startedNetwork) {
+ logTestcontainers('creating network...');
+ startedNetwork = await new Network().start();
+ logTestcontainers('network created.');
+ }
+ return startedNetwork;
+}
+
+/**
+ * The bridge network's gateway IP (e.g. 172.18.0.1) — a real address bound to an interface on the
+ * Docker host itself, so a process listening on 0.0.0.0 on the host is reachable both from the
+ * host directly and from any container on this network, without a network alias or mapped port.
+ * Takes a network id/name rather than a StartedNetwork object so it also works from
+ * reuseExistingStack(), which only has testConfig's network name.
+ */
+export async function getNetworkGatewayIp(network: string): Promise {
+ const {stdout} = await execFileAsync('docker', [
+ 'network',
+ 'inspect',
+ network,
+ '--format',
+ '{{(index .IPAM.Config 0).Gateway}}',
+ ]);
+ return stdout.trim();
+}
+
+export async function stopNetwork(): Promise {
+ if (!startedNetwork) {
+ return;
+ }
+
+ const network = startedNetwork;
+ startedNetwork = undefined;
+
+ // A container a worker process swapped in via restartMattermostContainer() (a different OS
+ // process, invisible to this one) can still be mid-detach from the network at this exact
+ // moment, which Docker reports as "has active endpoints". Ryuk removes the network anyway
+ // once that settles, so retry briefly rather than surfacing a scary but harmless error.
+ const attempts = 5;
+ for (let attempt = 1; attempt <= attempts; attempt++) {
+ try {
+ await network.stop();
+ return;
+ } catch (error) {
+ if (attempt === attempts) {
+ warnTestcontainers(
+ `could not remove network ${network.getId()} (Ryuk will remove it shortly): ${String(error)}`,
+ );
+ return;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/containers/openldap_container.ts b/e2e-tests/playwright/lib/src/containers/openldap_container.ts
new file mode 100644
index 000000000000..2c7ddc6ac8d2
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/openldap_container.ts
@@ -0,0 +1,37 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {GenericContainer, Wait} from 'testcontainers';
+import type {StartedNetwork, StartedTestContainer} from 'testcontainers';
+
+import {OPENLDAP_ADMIN_PASSWORD, OPENLDAP_ALIAS, OPENLDAP_PORT, TESTCONTAINERS_LABELS} from './constants';
+import {OPENLDAP_IMAGE} from './default_images';
+import {startWithRetry} from './retry';
+
+import {testConfig} from '@/test_config';
+
+// osixia/openldap is known to occasionally fail its first boot under load — startWithRetry
+// retries a bounded number of times rather than letting a flaky first attempt fail the whole run.
+export async function startOpenldapContainer(network: StartedNetwork): Promise {
+ return startWithRetry('openldap', async () => {
+ let builder = new GenericContainer(OPENLDAP_IMAGE)
+ .withExposedPorts(OPENLDAP_PORT)
+ .withNetwork(network)
+ .withNetworkAliases(OPENLDAP_ALIAS)
+ .withLabels(TESTCONTAINERS_LABELS)
+ .withEnvironment({
+ LDAP_TLS_VERIFY_CLIENT: 'never',
+ LDAP_ORGANISATION: 'Mattermost Test',
+ LDAP_DOMAIN: 'mm.test.com',
+ LDAP_ADMIN_PASSWORD: OPENLDAP_ADMIN_PASSWORD,
+ })
+ .withStartupTimeout(2 * 60_000)
+ .withWaitStrategy(Wait.forListeningPorts());
+
+ if (testConfig.testcontainersReuse) {
+ builder = builder.withReuse();
+ }
+
+ return builder.start();
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/containers/opensearch_container.ts b/e2e-tests/playwright/lib/src/containers/opensearch_container.ts
new file mode 100644
index 000000000000..dc669d46a22c
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/opensearch_container.ts
@@ -0,0 +1,46 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {GenericContainer, Wait} from 'testcontainers';
+import type {StartedNetwork, StartedTestContainer} from 'testcontainers';
+
+import {OPENSEARCH_ADMIN_PASSWORD, OPENSEARCH_ALIAS, OPENSEARCH_PORT, TESTCONTAINERS_LABELS} from './constants';
+import {OPENSEARCH_VERSION as DEFAULT_OPENSEARCH_VERSION} from './default_images';
+import {containerAssetPath} from './paths';
+import {startWithRetry} from './retry';
+
+import {testConfig} from '@/test_config';
+
+// Built from a vendored Dockerfile (installs the same CJK analysis plugins as the
+// Elasticsearch container) — an alternative to Elasticsearch for search.
+export async function startOpensearchContainer(network: StartedNetwork): Promise {
+ return startWithRetry('opensearch', async () => {
+ // deleteOnExit: false — otherwise the built image bakes in this session's Ryuk id, so Ryuk
+ // reaps the "reused" container the moment this session ends, defeating withReuse() below.
+ const image = await GenericContainer.fromDockerfile(containerAssetPath(), 'Dockerfile.opensearch')
+ .withBuildArgs({OPENSEARCH_VERSION: process.env.OPENSEARCH_VERSION || DEFAULT_OPENSEARCH_VERSION})
+ .build(undefined, {deleteOnExit: false});
+
+ let builder = image
+ .withExposedPorts(OPENSEARCH_PORT)
+ .withNetwork(network)
+ .withNetworkAliases(OPENSEARCH_ALIAS)
+ .withLabels(TESTCONTAINERS_LABELS)
+ .withEnvironment({
+ 'http.port': String(OPENSEARCH_PORT),
+ 'discovery.type': 'single-node',
+ 'plugins.security.disabled': 'true',
+ DISABLE_INSTALL_DEMO_CONFIG: 'true',
+ OPENSEARCH_INITIAL_ADMIN_PASSWORD: OPENSEARCH_ADMIN_PASSWORD,
+ OPENSEARCH_JAVA_OPTS: '-Xms512m -Xmx512m',
+ })
+ .withStartupTimeout(3 * 60_000)
+ .withWaitStrategy(Wait.forHttp('/_cluster/health', OPENSEARCH_PORT).forStatusCode(200));
+
+ if (testConfig.testcontainersReuse) {
+ builder = builder.withReuse();
+ }
+
+ return builder.start();
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/containers/paths.ts b/e2e-tests/playwright/lib/src/containers/paths.ts
new file mode 100644
index 000000000000..612705cf1aef
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/paths.ts
@@ -0,0 +1,19 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import path from 'node:path';
+
+// Resolved relative to this module's own location (not the caller's cwd or the monorepo), so it
+// keeps working once `@mattermost/playwright-lib` is installed as an npm package with no access
+// to the rest of the repo. `preserveModules` keeps `containers/assets` alongside this file's
+// compiled output in `dist`, same as `src`.
+//
+// `__dirname` rather than `import.meta.url`: despite `"type": "module"`, Playwright loads this
+// package via `require()`, not `import()` — and Node's require()-of-ESM interop disallows
+// `import.meta` (throws "Cannot use 'import.meta' outside a module"), while `__dirname` still
+// resolves since Node wraps the module as CommonJS to support that require() call.
+const assetsDir = path.join(__dirname, 'assets');
+
+export function containerAssetPath(...segments: string[]): string {
+ return path.join(assetsDir, ...segments);
+}
diff --git a/e2e-tests/playwright/lib/src/containers/postgres_container.ts b/e2e-tests/playwright/lib/src/containers/postgres_container.ts
new file mode 100644
index 000000000000..d2860f706d2b
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/postgres_container.ts
@@ -0,0 +1,40 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {PostgreSqlContainer} from '@testcontainers/postgresql';
+import type {StartedPostgreSqlContainer} from '@testcontainers/postgresql';
+import {Wait} from 'testcontainers';
+import type {StartedNetwork} from 'testcontainers';
+
+import {POSTGRES_ALIAS, POSTGRES_DB, POSTGRES_PASSWORD, POSTGRES_USER, TESTCONTAINERS_LABELS} from './constants';
+import {POSTGRES_IMAGE} from './default_images';
+import {containerAssetPath} from './paths';
+import {startWithRetry} from './retry';
+
+import {testConfig} from '@/test_config';
+
+export async function startPostgresContainer(network: StartedNetwork): Promise {
+ return startWithRetry('postgres', async () => {
+ let builder = new PostgreSqlContainer(POSTGRES_IMAGE)
+ .withDatabase(POSTGRES_DB)
+ .withUsername(POSTGRES_USER)
+ .withPassword(POSTGRES_PASSWORD)
+ .withNetwork(network)
+ .withNetworkAliases(POSTGRES_ALIAS)
+ .withLabels(TESTCONTAINERS_LABELS)
+ .withCopyFilesToContainer([
+ {
+ source: containerAssetPath('postgres.conf'),
+ target: '/etc/postgresql/postgresql.conf',
+ },
+ ])
+ .withCommand(['postgres', '-c', 'config_file=/etc/postgresql/postgresql.conf'])
+ .withWaitStrategy(Wait.forLogMessage(/database system is ready to accept connections/, 1));
+
+ if (testConfig.testcontainersReuse) {
+ builder = builder.withReuse();
+ }
+
+ return builder.start();
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/containers/requirements.ts b/e2e-tests/playwright/lib/src/containers/requirements.ts
new file mode 100644
index 000000000000..9384eade9408
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/requirements.ts
@@ -0,0 +1,27 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import type {StartedNetwork, StartedTestContainer} from 'testcontainers';
+
+import {startAzuriteContainer} from './azurite_container';
+import {startElasticsearchContainer} from './elasticsearch_container';
+import {startKeycloakContainer} from './keycloak_container';
+import {startMinioContainer} from './minio_container';
+import {startOpenldapContainer} from './openldap_container';
+import {startOpensearchContainer} from './opensearch_container';
+
+import type {TestContainersServiceName} from '@/test_config';
+
+// The single place to extend when a new additional service is needed. `stack.ts` starts these
+// only for the names in testConfig.testcontainersServices.
+export const ADDITIONAL_SERVICE_STARTERS: Record<
+ TestContainersServiceName,
+ (network: StartedNetwork) => Promise
+> = {
+ openldap: startOpenldapContainer,
+ keycloak: startKeycloakContainer,
+ elasticsearch: startElasticsearchContainer,
+ opensearch: startOpensearchContainer,
+ minio: startMinioContainer,
+ azurite: startAzuriteContainer,
+};
diff --git a/e2e-tests/playwright/lib/src/containers/retry.ts b/e2e-tests/playwright/lib/src/containers/retry.ts
new file mode 100644
index 000000000000..68519b825f44
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/retry.ts
@@ -0,0 +1,38 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {errorTestcontainers} from './log';
+
+import {duration, wait} from '@/util';
+
+const DEFAULT_ATTEMPTS = 3;
+
+// Wraps a container's build+start so transient failures (image pull/build over a flaky network,
+// occasional first-boot flakiness) get a bounded, backed-off retry instead of failing the whole
+// run — and so any failure, at any attempt, is unambiguous about which container/image caused it,
+// rather than surfacing as testcontainers' own generic "Failed to build image"/"Failed to start
+// container" with no name attached.
+export async function startWithRetry(
+ label: string,
+ start: () => Promise,
+ attempts = DEFAULT_ATTEMPTS,
+): Promise {
+ let lastError: unknown;
+
+ for (let attempt = 1; attempt <= attempts; attempt++) {
+ try {
+ return await start();
+ } catch (error) {
+ lastError = error;
+ const message = error instanceof Error ? error.message : String(error);
+ errorTestcontainers(`"${label}" failed on attempt ${attempt}/${attempts}: ${message}`);
+ if (attempt < attempts) {
+ await wait(duration.two_sec * attempt);
+ }
+ }
+ }
+
+ throw new Error(`Failed to start "${label}" container after ${attempts} attempts: ${String(lastError)}`, {
+ cause: lastError,
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/containers/stack.ts b/e2e-tests/playwright/lib/src/containers/stack.ts
new file mode 100644
index 000000000000..e6fbedf22635
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/stack.ts
@@ -0,0 +1,657 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {execFile} from 'node:child_process';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import {promisify} from 'node:util';
+
+import {test} from '@playwright/test';
+import type {StartedPostgreSqlContainer} from '@testcontainers/postgresql';
+import type {StartedNetwork, StartedTestContainer} from 'testcontainers';
+
+import {
+ AZURITE_ALIAS,
+ AZURITE_BLOB_PORT,
+ ELASTICSEARCH_ALIAS,
+ ELASTICSEARCH_PORT,
+ INBUCKET_ALIAS,
+ INBUCKET_WEB_PORT,
+ KEYCLOAK_ALIAS,
+ KEYCLOAK_PORT,
+ MATTERMOST_ALIAS,
+ MATTERMOST_PORT,
+ MINIO_ALIAS,
+ MINIO_PORT,
+ OPENLDAP_ALIAS,
+ OPENLDAP_PORT,
+ OPENSEARCH_ALIAS,
+ OPENSEARCH_PORT,
+ POSTGRES_ALIAS,
+ POSTGRES_DB,
+ POSTGRES_PASSWORD,
+ POSTGRES_PORT,
+ POSTGRES_USER,
+ WEBHOOK_ALIAS,
+ WEBHOOK_PORT,
+} from './constants';
+import {
+ AZURITE_IMAGE,
+ ELASTICSEARCH_VERSION,
+ INBUCKET_IMAGE,
+ KEYCLOAK_IMAGE,
+ MINIO_IMAGE,
+ OPENLDAP_IMAGE,
+ OPENSEARCH_VERSION,
+ POSTGRES_IMAGE,
+} from './default_images';
+import {startInbucketContainer} from './inbucket_container';
+import {logTestcontainers} from './log';
+import {startMattermostContainer} from './mattermost_container';
+import {getNetwork, getNetworkGatewayIp, stopNetwork} from './network';
+import {startPostgresContainer} from './postgres_container';
+import {ADDITIONAL_SERVICE_STARTERS} from './requirements';
+import {startWebhookContainer} from './webhook_container';
+
+import {clearClientCache} from '@/server/client';
+import {defaultBootEnv, testConfig} from '@/test_config';
+import type {TestContainersServiceName} from '@/test_config';
+import {duration} from '@/util';
+
+const execFileAsync = promisify(execFile);
+
+const ENV_FILE_PATH = path.resolve(process.cwd(), '.env.testcontainers');
+const LOG_DIR = path.resolve(process.cwd(), 'logs');
+
+type StartedStack = {
+ network: StartedNetwork;
+ postgres: StartedPostgreSqlContainer;
+ inbucket: StartedTestContainer;
+ webhook: StartedTestContainer;
+ mattermost: StartedTestContainer;
+ additional: Partial>;
+};
+
+let started: StartedStack | undefined;
+// True if this process is running against a stack an EARLIER process created (via
+// reuseExistingStack()) rather than one it started itself — there are no real Testcontainers
+// handles to hold in this case, only values .env.testcontainers resolved into testConfig via
+// dotenv. A reusing process never owns the stack's lifecycle, so stopStack() must leave both
+// the containers and the env file untouched for whatever other process still needs them.
+let reused = false;
+
+/**
+ * Brings up a bridge network, Postgres, Inbucket, the Mattermost server, and whichever
+ * additional services testConfig.testcontainersServices names. No-op if `testcontainers` mode isn't
+ * selected (PW_USE_TESTCONTAINERS unset), already started (repeated calls within the same
+ * process, e.g. a stray double-invocation, are harmless), or already reused. Otherwise first
+ * checks whether an earlier process's stack is still alive and reuses it instead — see
+ * reuseExistingStack().
+ */
+export async function startStack(): Promise {
+ if (!testConfig.useTestContainers || started || reused) {
+ return;
+ }
+
+ if (await reuseExistingStack()) {
+ return;
+ }
+
+ // reuseExistingStack() found nothing live to reuse — any bootEnvOverrides read from a stale
+ // .env.testcontainers (e.g. left behind by a manual `docker rm` or a crashed prior process)
+ // no longer describes anything real. Reset to the genuine defaults the container about to be
+ // created will actually boot with, or a later restart could wrongly believe some stale
+ // setting is already active and skip a restart it actually needs.
+ testConfig.bootEnvOverrides = defaultBootEnv();
+
+ const network = await getNetwork();
+
+ if (testConfig.containerRunner) {
+ await joinSelfToNetwork(network.getId());
+ }
+
+ // Stored so a host-side mock file server can be reached from both the browser and containers
+ // on this network (see getNetworkGatewayIp).
+ testConfig.testcontainersNetworkGatewayIp = await getNetworkGatewayIp(network.getId());
+
+ const additionalNames = testConfig.testcontainersServices;
+
+ logTestcontainers(
+ `pulling/starting images: server, postgres, inbucket, webhook${additionalNames.length ? `, ${additionalNames.join(', ')}` : ''}`,
+ );
+ await logServerImageAge(testConfig.serverImage);
+
+ // Tracks every container that actually comes up, independent of whether the group as a whole
+ // (or the mattermost start after it) ultimately succeeds — so a failure partway through still
+ // knows exactly what to tear down instead of leaking whatever already started.
+ const startedContainers: StartedTestContainer[] = [];
+ const trackAndLog = (name: string, promise: Promise): Promise => {
+ const startedAt = Date.now();
+
+ // The biggest blind spot is the server: its own wait strategy alone can take minutes
+ // (see mattermost_container.ts), during which nothing else prints — so ping every 30s to
+ // make clear the run hasn't stalled.
+ const heartbeat = setInterval(() => {
+ logTestcontainers(`still waiting on ${name} (${elapsedSeconds(startedAt)}s elapsed)...`);
+ }, duration.half_min);
+
+ return promise.then(
+ (container) => {
+ clearInterval(heartbeat);
+ startedContainers.push(container);
+ logTestcontainers(`${name} ready in ${elapsedSeconds(startedAt)}s.`);
+ return container;
+ },
+ (error) => {
+ clearInterval(heartbeat);
+ throw error;
+ },
+ );
+ };
+
+ try {
+ const [postgres, inbucket, webhook, ...additionalContainers] = await Promise.all([
+ trackAndLog('postgres', startPostgresContainer(network)),
+ trackAndLog('inbucket', startInbucketContainer(network)),
+ trackAndLog('webhook', startWebhookContainer(network)),
+ ...additionalNames.map((name) => trackAndLog(name, ADDITIONAL_SERVICE_STARTERS[name](network))),
+ ]);
+
+ const additional: Partial> = {};
+ additionalNames.forEach((name, index) => {
+ additional[name] = additionalContainers[index];
+ });
+
+ const mattermost = await trackAndLog('server', startMattermostContainer(network.getName()));
+
+ started = {network, postgres, inbucket, webhook, mattermost, additional};
+ } catch (error) {
+ await Promise.allSettled(startedContainers.map((container) => container.stop()));
+ await stopNetwork();
+ throw error;
+ }
+
+ applyResolvedConfig(started);
+ resetEnvFile('initial boot');
+
+ logStackStarted(started);
+}
+
+/**
+ * True if .env.testcontainers points at a Mattermost container that's still running — i.e. some
+ * OTHER process already brought up a stack this process should reuse instead of duplicating.
+ * Always a different process (the only channel between them is the env file, not runtime IPC);
+ * covers PW_TESTCONTAINERS_REUSE=true across separate local invocations, and a CI dispatcher that
+ * starts the server once per worker job and runs one spec per process against it.
+ *
+ * Deliberately does not gate on testConfig.testcontainersReuse: that flag governs whether the
+ * OWNING process leaves the stack running on its own exit — a different decision from whether
+ * THIS process should reuse a stack it finds already alive. Reuse always applies once liveness
+ * is confirmed.
+ */
+async function reuseExistingStack(): Promise {
+ if (!testConfig.mattermostContainerId || !(await isContainerRunning(testConfig.mattermostContainerId))) {
+ return false;
+ }
+
+ reused = true;
+
+ if (testConfig.containerRunner) {
+ await joinSelfToNetwork(testConfig.testcontainersNetworkName);
+ }
+
+ logTestcontainers(
+ 'reusing already-running server (with PW_TESTCONTAINERS_REUSE=true), see .env.testcontainers for stack information',
+ );
+ logStackReused();
+ return true;
+}
+
+// Same shape of summary as logStackStarted(), but built from testConfig's resolved fields
+// instead of live StartedTestContainer handles — reusing never gets those (see `reused`'s
+// declaration above), only whatever an EARLIER process's startStack() persisted to
+// .env.testcontainers and this process's dotenv.config() read back into testConfig.
+function logStackReused(): void {
+ const lines: string[] = [
+ ` - ${'server'.padEnd(13)} = ${testConfig.baseURL}`,
+ ` - ${'postgres'.padEnd(13)} = ${testConfig.postgresUrl}`,
+ ` - ${'inbucket'.padEnd(13)} = ${testConfig.smtpURL}`,
+ ` - ${'webhook'.padEnd(13)} = ${testConfig.webhookBaseUrl}`,
+ ];
+
+ const additionalUrls: Record = {
+ openldap: `${testConfig.ldapHost}:${testConfig.ldapPort}`,
+ keycloak: testConfig.keycloakUrl,
+ elasticsearch: testConfig.elasticsearchUrl,
+ opensearch: testConfig.opensearchUrl,
+ minio: testConfig.minioUrl,
+ azurite: testConfig.azuriteUrl,
+ };
+ testConfig.testcontainersServices.forEach((name) => {
+ lines.push(` - ${name.padEnd(13)} = ${additionalUrls[name]}`);
+ });
+
+ // eslint-disable-next-line no-console
+ console.log(
+ `Testcontainers (reused, network ${testConfig.testcontainersNetworkName}, tear down with: "npm run testcontainers:down"):\n${lines.join('\n')}\n`,
+ );
+}
+
+async function isContainerRunning(containerId: string): Promise {
+ try {
+ const {stdout} = await execFileAsync('docker', ['inspect', '-f', '{{.State.Running}}', containerId]);
+ return stdout.trim() === 'true';
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Tears down the stack: always collects logs and removes the generated env file; only actually
+ * stops containers when reuse isn't enabled (PW_TESTCONTAINERS_REUSE=true leaves them running
+ * for the next invocation — local or a CI dispatcher's next spec — to reuse).
+ *
+ * A no-op beyond clearing the local flag when this process reused rather than created the stack:
+ * it never owned the containers or the env file, so it must leave both exactly as it found them
+ * for whichever process (or later dispatch) still depends on them.
+ */
+export async function stopStack(options: {force?: boolean} = {}): Promise {
+ if (!testConfig.useTestContainers) {
+ return;
+ }
+
+ if (reused) {
+ reused = false;
+ logTestcontainers('this process reused an existing server — leaving it untouched.');
+ return;
+ }
+
+ if (!started) {
+ return;
+ }
+
+ const stack = started;
+ await collectLogs(stack);
+
+ const shouldStop = options.force || !testConfig.testcontainersReuse;
+ if (shouldStop) {
+ await Promise.allSettled([
+ stack.mattermost.stop(),
+ stack.inbucket.stop(),
+ stack.webhook.stop(),
+ stack.postgres.stop(),
+ ...Object.values(stack.additional).map((container) => container?.stop()),
+ ]);
+ await stopNetwork();
+ logStackStopped(stack);
+ archiveEnvFile();
+ removeEnvFile();
+ } else {
+ logStackLeftRunning(stack);
+ }
+
+ started = undefined;
+}
+
+/**
+ * True if every key in `env` already has the given value in testConfig.bootEnvOverrides — i.e.
+ * the currently-running Mattermost container was already booted this way, so a pw.ensure*() can
+ * skip restartMattermostContainer() for these settings.
+ */
+export function bootEnvMatches(env: Record): boolean {
+ return Object.entries(env).every(([key, value]) => testConfig.bootEnvOverrides[key] === value);
+}
+
+/**
+ * Stops the current Mattermost container and starts a fresh one with additional env merged in —
+ * used for settings like FileSettings.DriverName, ElasticsearchSettings.Backend, or
+ * MM_FEATUREFLAGS_* which are never re-read from a running server, so patchConfig alone can't
+ * change them.
+ *
+ * Safe to call from a Playwright worker process, unlike startStack()/stopStack(): it works from
+ * testConfig's container id and network name rather than the in-process StartedStack, since
+ * global setup (which owns that in-process state) and worker processes are different OS
+ * processes.
+ *
+ * `env` is merged into testConfig.bootEnvOverrides (not replaced) so an earlier pw.ensure*()
+ * call's settings survive a later, unrelated one restarting the same container again. The merged
+ * result, new container id, and new baseURL are appended to .env.testcontainers so any other
+ * process picks up the real current state instead of a stale or default one.
+ *
+ * Always performs the restart without checking whether `env` is already active — callers (e.g.
+ * ensureMinio()/ensureAzurite()) own that decision via bootEnvMatches().
+ */
+export async function restartMattermostContainer(env: Record): Promise {
+ if (!testConfig.useTestContainers) {
+ throw new Error('restartMattermostContainer requires PW_USE_TESTCONTAINERS=true.');
+ }
+ if (!testConfig.mattermostContainerId || !testConfig.testcontainersNetworkName) {
+ throw new Error(
+ 'No running Testcontainers stack to restart (missing Mattermost container id or network name).',
+ );
+ }
+
+ extendTimeoutForRestart();
+
+ testConfig.bootEnvOverrides = {...testConfig.bootEnvOverrides, ...env};
+
+ await execFileAsync('docker', ['rm', '-f', testConfig.mattermostContainerId]);
+
+ const mattermost = await startMattermostContainer(
+ testConfig.testcontainersNetworkName,
+ testConfig.bootEnvOverrides,
+ );
+
+ testConfig.baseURL = resolveUrl(mattermost, MATTERMOST_PORT, MATTERMOST_ALIAS);
+ testConfig.mattermostContainerId = mattermost.getId();
+ clearClientCache();
+
+ appendEnvFile(`restart requested by ${describeCurrentTest()} — env ${JSON.stringify(env)}`);
+
+ logTestcontainers(`restarted server with ${JSON.stringify(env)}.`);
+}
+
+// Identifies whichever spec/test is currently driving a restart, so .env.testcontainers's history
+// shows why the server ended up in its current state. restartMattermostContainer() is always
+// called from inside a running test, so test.info() should resolve; the fallback only guards a
+// future caller that isn't.
+function describeCurrentTest(): string {
+ try {
+ const info = test.info();
+ return `${path.relative(process.cwd(), info.file)} > ${info.title}`;
+ } catch {
+ return 'unknown caller (not running inside a test)';
+ }
+}
+
+// startMattermostContainer()'s wait strategy blocks on a scheduler log line whose first tick is
+// deliberately delayed 60s after startup (see that function's comment), so a restart alone can
+// exceed the suite's default 60s test timeout before the test has done any of its own work.
+// Playwright's timeout wouldn't cancel the still-in-flight restart, so a timed-out retry can race
+// it and hit a container mid-swap. Raising (not just extending) the timeout avoids ratcheting it
+// down if a later restart in the same test calls this again after some budget is already spent.
+function extendTimeoutForRestart(): void {
+ try {
+ const info = test.info();
+ info.setTimeout(Math.max(info.timeout, duration.four_min));
+ } catch {
+ // Not running inside a test (e.g. called from a script) — nothing to extend.
+ }
+}
+
+// containerRunner mode: join the calling `playwright` container to the same network so its own
+// connections can use aliases too, instead of mapped ports. Relies on Docker setting the
+// container's hostname to its own container ID by default, and on the `docker` CLI being
+// present alongside the mounted socket.
+//
+// Takes a network name/ID string rather than a StartedNetwork object: reuseExistingStack() only
+// has testConfig.testcontainersNetworkName (read from .env.testcontainers) to work with, not a
+// live handle — and the docker CLI resolves either form the same way, so the freshly-created path
+// below just passes network.getId() instead.
+async function joinSelfToNetwork(networkId: string): Promise {
+ const selfContainerId = os.hostname();
+ try {
+ await execFileAsync('docker', ['network', 'connect', networkId, selfContainerId]);
+ } catch (error) {
+ // A CI dispatcher running one spec per process reuses the same stack (and this same
+ // runner container) on every invocation, so this join is attempted again every time —
+ // already-connected isn't a failure, it's the expected steady state after the first.
+ if (String(error).includes('already exists in network')) {
+ return;
+ }
+ throw new Error(
+ 'containerRunner mode (PW_TESTCONTAINERS_CONTAINER_RUNNER=true) requires the calling container ' +
+ 'to join the Testcontainers network, but "docker network connect" failed for container ' +
+ `"${selfContainerId}": ${String(error)}. Ensure /var/run/docker.sock is mounted and the docker ` +
+ 'CLI is installed in this image.',
+ );
+ }
+}
+
+function resolveUrl(container: StartedTestContainer, port: number, alias: string): string {
+ if (testConfig.containerRunner) {
+ return `http://${alias}:${port}`;
+ }
+ return `http://${container.getHost()}:${container.getMappedPort(port)}`;
+}
+
+function resolveHostAndPort(container: StartedTestContainer, port: number, alias: string): [string, number] {
+ if (testConfig.containerRunner) {
+ return [alias, port];
+ }
+ return [container.getHost(), container.getMappedPort(port)];
+}
+
+// Same containerRunner-aware resolution as resolveUrl()/resolveHostAndPort() above: the alias in
+// containerRunner mode (the test process is on the Testcontainers network), the host-mapped port
+// otherwise — direct-DB specs are just another client connecting from wherever the test process
+// runs.
+function resolvePostgresUrl(postgres: StartedPostgreSqlContainer): string {
+ const [host, port] = resolveHostAndPort(postgres, POSTGRES_PORT, POSTGRES_ALIAS);
+ return `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${host}:${port}/${POSTGRES_DB}?sslmode=disable&connect_timeout=10&binary_parameters=yes`;
+}
+
+type ContainerMetadata = {alias: string; port: number; image: string};
+
+const ADDITIONAL_CONTAINER_METADATA: Record = {
+ openldap: {alias: OPENLDAP_ALIAS, port: OPENLDAP_PORT, image: OPENLDAP_IMAGE},
+ keycloak: {alias: KEYCLOAK_ALIAS, port: KEYCLOAK_PORT, image: KEYCLOAK_IMAGE},
+ elasticsearch: {
+ alias: ELASTICSEARCH_ALIAS,
+ port: ELASTICSEARCH_PORT,
+ image: `built, Elasticsearch ${ELASTICSEARCH_VERSION}`,
+ },
+ opensearch: {alias: OPENSEARCH_ALIAS, port: OPENSEARCH_PORT, image: `built, OpenSearch ${OPENSEARCH_VERSION}`},
+ minio: {alias: MINIO_ALIAS, port: MINIO_PORT, image: MINIO_IMAGE},
+ azurite: {alias: AZURITE_ALIAS, port: AZURITE_BLOB_PORT, image: AZURITE_IMAGE},
+};
+
+function containerEntries(stack: StartedStack): Array<[string, StartedTestContainer, ContainerMetadata]> {
+ const base: Array<[string, StartedTestContainer, ContainerMetadata]> = [
+ ['server', stack.mattermost, {alias: MATTERMOST_ALIAS, port: MATTERMOST_PORT, image: testConfig.serverImage}],
+ ['postgres', stack.postgres, {alias: POSTGRES_ALIAS, port: POSTGRES_PORT, image: POSTGRES_IMAGE}],
+ ['inbucket', stack.inbucket, {alias: INBUCKET_ALIAS, port: INBUCKET_WEB_PORT, image: INBUCKET_IMAGE}],
+ ['webhook', stack.webhook, {alias: WEBHOOK_ALIAS, port: WEBHOOK_PORT, image: 'built, webhook sidecar'}],
+ ];
+
+ const additional = Object.entries(stack.additional)
+ .filter((entry): entry is [TestContainersServiceName, StartedTestContainer] => entry[1] !== undefined)
+ .map((entry): [string, StartedTestContainer, ContainerMetadata] => [
+ entry[0],
+ entry[1],
+ ADDITIONAL_CONTAINER_METADATA[entry[0]],
+ ]);
+
+ return [...base, ...additional];
+}
+
+function formatContainerLine(name: string, container: StartedTestContainer, metadata: ContainerMetadata): string {
+ const host = `${container.getHost()}:${container.getMappedPort(metadata.port)}`;
+ return ` - ${name.padEnd(13)} = ${metadata.image} (network: ${metadata.alias}:${metadata.port}, host: ${host})`;
+}
+
+function elapsedSeconds(startedAt: number): string {
+ return ((Date.now() - startedAt) / 1000).toFixed(1);
+}
+
+// `master`/`release-*` tags get rebuilt continuously, so a cached copy can silently go stale;
+// pinned version tags (e.g. `:11.10.0`) never change, so they're excluded.
+const MUTABLE_IMAGE_TAG_PATTERN = /:(master|release-.+)$/;
+
+async function logServerImageAge(image: string): Promise {
+ let created: Date;
+ try {
+ const {stdout} = await execFileAsync('docker', ['image', 'inspect', image, '--format', '{{.Created}}']);
+ created = new Date(stdout.trim());
+ } catch {
+ // Not cached locally — Testcontainers will pull it fresh as part of starting the
+ // container, so whatever comes up is already the latest build. Nothing to warn about.
+ logTestcontainers(`server image "${image}" isn't cached locally yet — will pull the latest build.`);
+ return;
+ }
+
+ const ageHours = (Date.now() - created.getTime()) / (60 * 60 * 1000);
+ const age = ageHours >= 48 ? `${(ageHours / 24).toFixed()}d` : `${ageHours.toFixed()}h`;
+ const looksStale = MUTABLE_IMAGE_TAG_PATTERN.test(image) && ageHours > 24;
+
+ logTestcontainers(
+ `server image "${image}" (built ${created.toISOString()}, ${age} ago).` +
+ (looksStale
+ ? ` This is a moving tag and the cached copy may be outdated — run "docker pull ${image}" for the latest build.`
+ : ''),
+ );
+}
+
+function logStackStarted(stack: StartedStack): void {
+ const lines = containerEntries(stack).map(([name, container, metadata]) =>
+ formatContainerLine(name, container, metadata),
+ );
+ // eslint-disable-next-line no-console
+ console.log(`Testcontainers (network ${stack.network.getId()}):\n${lines.join('\n')}`);
+}
+
+function logStackStopped(stack: StartedStack): void {
+ const names = containerEntries(stack).map(([name]) => name);
+ logTestcontainers(`stopped ${names.join(', ')}.`);
+}
+
+function logStackLeftRunning(stack: StartedStack): void {
+ const serverUrl = resolveUrl(stack.mattermost, MATTERMOST_PORT, MATTERMOST_ALIAS);
+ logTestcontainers(
+ `left running (PW_TESTCONTAINERS_REUSE=true) — server reachable at ${serverUrl}; may tear down with: "npm run testcontainers:down"`,
+ );
+}
+
+// Mutates the testConfig singleton in place so the rest of globalSetup (same process) sees the
+// real resolved values immediately — the generated env file (resetEnvFile) is what hands these
+// same values to worker processes.
+function applyResolvedConfig(stack: StartedStack): void {
+ testConfig.baseURL = resolveUrl(stack.mattermost, MATTERMOST_PORT, MATTERMOST_ALIAS);
+ testConfig.smtpURL = resolveUrl(stack.inbucket, INBUCKET_WEB_PORT, INBUCKET_ALIAS);
+ testConfig.postgresUrl = resolvePostgresUrl(stack.postgres);
+ testConfig.webhookBaseUrl = resolveUrl(stack.webhook, WEBHOOK_PORT, WEBHOOK_ALIAS);
+ testConfig.testcontainersNetworkName = stack.network.getName();
+ testConfig.mattermostContainerId = stack.mattermost.getId();
+
+ if (stack.additional.openldap) {
+ const [host, port] = resolveHostAndPort(stack.additional.openldap, OPENLDAP_PORT, OPENLDAP_ALIAS);
+ testConfig.ldapHost = host;
+ testConfig.ldapPort = port;
+ }
+ if (stack.additional.keycloak) {
+ testConfig.keycloakUrl = resolveUrl(stack.additional.keycloak, KEYCLOAK_PORT, KEYCLOAK_ALIAS);
+ }
+ if (stack.additional.elasticsearch) {
+ testConfig.elasticsearchUrl = resolveUrl(
+ stack.additional.elasticsearch,
+ ELASTICSEARCH_PORT,
+ ELASTICSEARCH_ALIAS,
+ );
+ }
+ if (stack.additional.opensearch) {
+ testConfig.opensearchUrl = resolveUrl(stack.additional.opensearch, OPENSEARCH_PORT, OPENSEARCH_ALIAS);
+ }
+ if (stack.additional.minio) {
+ testConfig.minioUrl = resolveUrl(stack.additional.minio, MINIO_PORT, MINIO_ALIAS);
+ }
+ if (stack.additional.azurite) {
+ testConfig.azuriteUrl = resolveUrl(stack.additional.azurite, AZURITE_BLOB_PORT, AZURITE_ALIAS);
+ }
+}
+
+// One block per write: a human-readable `# [timestamp] label` comment line (dotenv ignores
+// `#`-led lines) followed by the current resolved KEY=VALUE state, including bootEnvOverrides
+// JSON-encoded and single-quoted so its embedded double quotes/braces survive dotenv's parser.
+function envFileLines(label: string): string[] {
+ return [
+ `# [${new Date().toISOString()}] ${label}`,
+ `PW_BASE_URL=${testConfig.baseURL}`,
+ `PW_SMTP_URL=${testConfig.smtpURL}`,
+ `PW_POSTGRES_URL=${testConfig.postgresUrl}`,
+ `PW_WEBHOOK_BASE_URL=${testConfig.webhookBaseUrl}`,
+ `PW_TESTCONTAINERS_NETWORK_GATEWAY_IP=${testConfig.testcontainersNetworkGatewayIp}`,
+ `PW_LDAP_HOST=${testConfig.ldapHost}`,
+ `PW_LDAP_PORT=${testConfig.ldapPort}`,
+ `PW_KEYCLOAK_URL=${testConfig.keycloakUrl}`,
+ `PW_ELASTICSEARCH_URL=${testConfig.elasticsearchUrl}`,
+ `PW_OPENSEARCH_URL=${testConfig.opensearchUrl}`,
+ `PW_MINIO_URL=${testConfig.minioUrl}`,
+ `PW_AZURITE_URL=${testConfig.azuriteUrl}`,
+ `PW_TESTCONTAINERS_NETWORK_NAME=${testConfig.testcontainersNetworkName}`,
+ `PW_TESTCONTAINERS_MATTERMOST_CONTAINER_ID=${testConfig.mattermostContainerId}`,
+ `PW_TESTCONTAINERS_BOOT_ENV='${JSON.stringify(testConfig.bootEnvOverrides)}'`,
+ '',
+ ];
+}
+
+// (Re)creates .env.testcontainers from scratch — only called once, when a brand new stack boots,
+// so a leftover file from an earlier (now-dead) stack never bleeds into this one.
+function resetEnvFile(label: string): void {
+ fs.writeFileSync(ENV_FILE_PATH, envFileLines(label).join('\n') + '\n', 'utf-8');
+}
+
+/**
+ * Appends a new snapshot block instead of overwriting — dotenv resolves the correct current value
+ * per key when a fresh process parses the file (later occurrences win), while the file as a whole
+ * becomes a chronological log of every restart: which spec/test triggered it, what env diff was
+ * requested, and the full resolved state right after. That's what's needed to investigate
+ * server-state drift after the fact, since in the CI dispatch model no single process ever sees
+ * the whole picture on its own.
+ */
+function appendEnvFile(label: string): void {
+ fs.appendFileSync(ENV_FILE_PATH, envFileLines(label).join('\n') + '\n', 'utf-8');
+}
+
+// Preserves the full restart history as a debug artifact before it's deleted — logs/ is already
+// what CI's upload-debug-artifacts step picks up, so this needs no separate wiring.
+function archiveEnvFile(): void {
+ if (!fs.existsSync(ENV_FILE_PATH)) {
+ return;
+ }
+ fs.mkdirSync(LOG_DIR, {recursive: true});
+ fs.copyFileSync(ENV_FILE_PATH, path.join(LOG_DIR, 'testcontainers_env_history.log'));
+}
+
+function removeEnvFile(): void {
+ if (fs.existsSync(ENV_FILE_PATH)) {
+ fs.rmSync(ENV_FILE_PATH);
+ }
+}
+
+async function collectLogs(stack: StartedStack): Promise {
+ fs.mkdirSync(LOG_DIR, {recursive: true});
+
+ const targets: Array<[string, StartedTestContainer]> = [
+ ['mattermost', stack.mattermost],
+ ['postgres', stack.postgres],
+ ['inbucket', stack.inbucket],
+ ['webhook', stack.webhook],
+ ...Object.entries(stack.additional).filter(
+ (entry): entry is [string, StartedTestContainer] => entry[1] !== undefined,
+ ),
+ ];
+
+ await Promise.allSettled(
+ targets.map(async ([name, container]) => {
+ const logStream = await container.logs();
+ const outFile = fs.createWriteStream(path.join(LOG_DIR, `${name}.log`));
+ await new Promise((resolve, reject) => {
+ logStream.pipe(outFile);
+ // Don't let a stalled log stream hold up teardown indefinitely.
+ const timer = setTimeout(() => {
+ outFile.end();
+ resolve();
+ }, 10_000);
+ logStream.on('end', () => {
+ clearTimeout(timer);
+ resolve();
+ });
+ logStream.on('error', (error) => {
+ clearTimeout(timer);
+ reject(error);
+ });
+ });
+ }),
+ );
+}
diff --git a/e2e-tests/playwright/lib/src/containers/webhook_container.ts b/e2e-tests/playwright/lib/src/containers/webhook_container.ts
new file mode 100644
index 000000000000..00642df9b50c
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/containers/webhook_container.ts
@@ -0,0 +1,38 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {GenericContainer, Wait} from 'testcontainers';
+import type {StartedNetwork, StartedTestContainer} from 'testcontainers';
+
+import {TESTCONTAINERS_LABELS, WEBHOOK_ALIAS, WEBHOOK_PORT} from './constants';
+import {containerAssetPath} from './paths';
+import {startWithRetry} from './retry';
+
+import {testConfig} from '@/test_config';
+
+// Vendored from e2e-tests/cypress/webhook_serve.js — the interactive-message/dialog callback
+// sidecar tests point PW_WEBHOOK_BASE_URL at. Always started, unlike the other optional services,
+// since testConfig.webhookBaseUrl defaults to localhost:3000 outside Testcontainers mode too.
+export async function startWebhookContainer(network: StartedNetwork): Promise {
+ return startWithRetry('webhook', async () => {
+ // deleteOnExit: false — otherwise the built image bakes in this session's Ryuk id, so Ryuk
+ // reaps the "reused" container the moment this session ends, defeating withReuse() below.
+ const image = await GenericContainer.fromDockerfile(containerAssetPath('webhook'), 'Dockerfile.webhook').build(
+ undefined,
+ {deleteOnExit: false},
+ );
+
+ let builder = image
+ .withExposedPorts(WEBHOOK_PORT)
+ .withNetwork(network)
+ .withNetworkAliases(WEBHOOK_ALIAS)
+ .withLabels(TESTCONTAINERS_LABELS)
+ .withWaitStrategy(Wait.forHttp('/', WEBHOOK_PORT).forStatusCode(200));
+
+ if (testConfig.testcontainersReuse) {
+ builder = builder.withReuse();
+ }
+
+ return builder.start();
+ });
+}
diff --git a/e2e-tests/playwright/lib/src/file_server.ts b/e2e-tests/playwright/lib/src/file_server.ts
index d843119e68f5..fe4d9416ab98 100644
--- a/e2e-tests/playwright/lib/src/file_server.ts
+++ b/e2e-tests/playwright/lib/src/file_server.ts
@@ -6,6 +6,8 @@ import path from 'node:path';
import {test} from './test_fixture';
+import {testConfig} from '@/test_config';
+
/**
* Starts a server that serves files from ./asset. When run from the monorepo, this will serve files from
* e2e-tests/playwright/asset.
@@ -48,7 +50,7 @@ export function setupFileServer(): Promise {
fileServer.once('message', (message: {type?: string; port?: number}) => {
if (message?.type === 'listening' && message.port) {
clearTimeout(timeout);
- resolve(`http://localhost:${message.port}`);
+ resolve(`http://${fileServerHost()}:${message.port}`);
}
});
@@ -60,3 +62,11 @@ export function setupFileServer(): Promise {
}
});
}
+
+// localhost isn't reachable from inside the Mattermost container in `testcontainers` mode (it resolves to
+// the container's own loopback, not the host's). testcontainersNetworkGatewayIp is reachable
+// from both the container and the host-side browser, so it works as a single URL for both — this
+// server binds to 0.0.0.0, not just loopback, to accept the former.
+function fileServerHost(): string {
+ return testConfig.useTestContainers ? testConfig.testcontainersNetworkGatewayIp : 'localhost';
+}
diff --git a/e2e-tests/playwright/lib/src/index.ts b/e2e-tests/playwright/lib/src/index.ts
index 4515f17f82e6..ef6df2b774a6 100644
--- a/e2e-tests/playwright/lib/src/index.ts
+++ b/e2e-tests/playwright/lib/src/index.ts
@@ -3,7 +3,8 @@
export {test, expect, PlaywrightExtended} from './test_fixture';
export type {ExtendedFixtures} from './test_fixture';
-export {testConfig} from './test_config';
+export {testConfig, TESTCONTAINERS_SERVICE_NAMES} from './test_config';
+export type {TestContainersServiceName} from './test_config';
export {baseGlobalSetup} from './global_setup';
export {TestBrowser} from './browser_context';
export {getBlobFromAsset, getFileFromAsset} from './file';
@@ -22,8 +23,33 @@ export {
isWebhookTestServerReachable,
setupWebhookTestServer,
PlaywrightClient4,
+ generateLdapUser,
+ createLdapUser,
+ updateLdapUser,
+ deleteLdapUser,
+ ldapServerConfig,
+ ensureOpenldap,
+ createKeycloakUser,
+ deleteKeycloakUser,
+ listMinioObjectKeys,
+ ensureMinio,
+ samlServerConfig,
+ ensureKeycloak,
+ elasticsearchServerConfig,
+ opensearchServerConfig,
+ ensureElasticsearch,
+ ensureOpensearch,
+ ensureAzurite,
+ listAzuriteBlobNames,
+ ensureLocalFile,
+ ensurePostgresSearch,
+ ensureFeatureFlag,
+ runMmctl,
+ ensureMmctl,
} from './server';
-export type {InbucketEmail} from './server';
+export type {InbucketEmail, LdapUser, KeycloakUser, MmctlResult} from './server';
+
+export {startStack, stopStack} from './containers';
export {
ChannelsPage,
diff --git a/e2e-tests/playwright/lib/src/server/azurite.ts b/e2e-tests/playwright/lib/src/server/azurite.ts
new file mode 100644
index 000000000000..a9b54c23da71
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/server/azurite.ts
@@ -0,0 +1,94 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {BlobServiceClient, StorageSharedKeyCredential} from '@azure/storage-blob';
+import {test} from '@playwright/test';
+
+import {
+ AZURITE_ACCOUNT_KEY,
+ AZURITE_ACCOUNT_NAME,
+ AZURITE_ALIAS,
+ AZURITE_BLOB_PORT,
+ AZURITE_CONTAINER,
+} from '../containers/constants';
+import {bootEnvMatches, restartMattermostContainer} from '../containers/stack';
+
+import {uploadProbeImage} from './filestore';
+import {getAdminClient} from './init';
+
+import {testConfig} from '@/test_config';
+
+function getBlobServiceClient(): BlobServiceClient {
+ const credential = new StorageSharedKeyCredential(AZURITE_ACCOUNT_NAME, AZURITE_ACCOUNT_KEY);
+ return new BlobServiceClient(`${testConfig.azuriteUrl}/${AZURITE_ACCOUNT_NAME}`, credential);
+}
+
+// Only used internally by ensureAzurite() — not a spec-facing entry point.
+async function ensureAzuriteContainer(container: string = AZURITE_CONTAINER): Promise {
+ const client = getBlobServiceClient().getContainerClient(container);
+ const exists = await client.exists();
+ if (!exists) {
+ await client.create();
+ }
+}
+
+/** Lists every blob name in the container, to confirm the server actually wrote to Azurite. */
+export async function listAzuriteBlobNames(container: string = AZURITE_CONTAINER): Promise {
+ const client = getBlobServiceClient().getContainerClient(container);
+ const names: string[] = [];
+ for await (const blob of client.listBlobsFlat()) {
+ names.push(blob.name);
+ }
+ return names;
+}
+
+// FileSettings' backend is chosen once when the Mattermost process boots and never re-read from a
+// running server's config, so pointing the server at Azurite can only happen via the env vars the
+// container starts with — always via the network alias, since the server itself always runs
+// inside the Testcontainers network. AzureEndpoint under the "custom" cloud is the full service
+// URL (path-style, account name included), unlike the vhost-style URLs real Azure uses.
+function azuriteServerEnv(): Record {
+ return {
+ MM_FILESETTINGS_DRIVERNAME: 'azureblob',
+ MM_FILESETTINGS_AZURESTORAGEACCOUNT: AZURITE_ACCOUNT_NAME,
+ MM_FILESETTINGS_AZUREAUTHMODE: 'shared_key',
+ MM_FILESETTINGS_AZUREACCESSKEY: AZURITE_ACCOUNT_KEY,
+ MM_FILESETTINGS_AZURECONTAINER: AZURITE_CONTAINER,
+ MM_FILESETTINGS_AZURECLOUD: 'custom',
+ MM_FILESETTINGS_AZUREENDPOINT: `http://${AZURITE_ALIAS}:${AZURITE_BLOB_PORT}/${AZURITE_ACCOUNT_NAME}`,
+ MM_FILESETTINGS_AZURESSL: 'false',
+ };
+}
+
+/**
+ * Checks Azurite was started this run, restarts the server onto it if it isn't already the active
+ * file storage backend, and confirms a real upload actually lands in Azurite — skipping the test
+ * otherwise, instead of failing on an unmet precondition.
+ */
+export async function ensureAzurite(): Promise {
+ if (!testConfig.testcontainersServices.includes('azurite')) {
+ test.skip(true, 'Skipping test - azurite not started (set PW_TESTCONTAINERS_SERVICES=azurite)');
+ return;
+ }
+
+ try {
+ await ensureAzuriteContainer();
+ const env = azuriteServerEnv();
+ if (!bootEnvMatches(env)) {
+ await restartMattermostContainer(env);
+ }
+
+ const {adminClient, adminUser} = await getAdminClient();
+ await uploadProbeImage(adminClient, adminUser);
+
+ const blobNames = await listAzuriteBlobNames();
+ if (blobNames.length === 0) {
+ throw new Error(
+ 'Azurite container is still empty after a real upload — the server is not actually using ' +
+ 'Azurite as its file backend.',
+ );
+ }
+ } catch (error) {
+ test.skip(true, `Skipping test - Azurite connection test failed: ${String(error)}`);
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/server/client.ts b/e2e-tests/playwright/lib/src/server/client.ts
index 60b7fbd49843..6ba8ada2f0d6 100644
--- a/e2e-tests/playwright/lib/src/server/client.ts
+++ b/e2e-tests/playwright/lib/src/server/client.ts
@@ -10,6 +10,17 @@ import {testConfig} from '@/test_config';
// Variable to hold cache
const clients: Record = {};
+/**
+ * Drops every cached client, so the next makeClient()/getAdminClient() call logs in again instead
+ * of reusing a session pointed at a Mattermost container that no longer exists — needed after
+ * restartMattermostContainer() swaps in a fresh container (new base URL, new session).
+ */
+export function clearClientCache(): void {
+ for (const key of Object.keys(clients)) {
+ delete clients[key];
+ }
+}
+
export async function makeClient(
userRequest?: UserRequest,
opts: {useCache?: boolean; skipLog?: boolean} = {useCache: true, skipLog: false},
diff --git a/e2e-tests/playwright/lib/src/server/default_config.ts b/e2e-tests/playwright/lib/src/server/default_config.ts
index edb6d7143092..0297cf1e3780 100644
--- a/e2e-tests/playwright/lib/src/server/default_config.ts
+++ b/e2e-tests/playwright/lib/src/server/default_config.ts
@@ -79,7 +79,12 @@ const onPremServerConfig = (): Partial => {
},
},
ServiceSettings: {
- SiteURL: testConfig.baseURL,
+ // SiteURL is the server's own view of itself (e.g. for building plugin callback
+ // URLs), so it must use an address the server can reach itself with. In `testcontainers` mode
+ // testConfig.baseURL is a host-mapped port the server's own container can't reach;
+ // internalBaseURL is the Docker network alias there, and the same as baseURL in
+ // `external` mode — correct in both cases.
+ SiteURL: testConfig.internalBaseURL,
EnableOnboardingFlow: false,
EnableSecurityFixAlert: false,
GiphySdkKey: 's0glxvzVg9azvPipKxcPLpXV0q1x1fVP',
@@ -858,6 +863,7 @@ const defaultServerConfig: AdminConfig = {
EnableChannelPolicyIndicators: true,
TrustProxyDeviceIdentityHeader: false,
EnforceDeviceIDConsistency: false,
+ EnableAccessControlAuditLogging: false,
},
ContentFlaggingSettings: {
EnableContentFlagging: false,
diff --git a/e2e-tests/playwright/lib/src/server/elasticsearch.ts b/e2e-tests/playwright/lib/src/server/elasticsearch.ts
new file mode 100644
index 000000000000..ad4578743b05
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/server/elasticsearch.ts
@@ -0,0 +1,52 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@playwright/test';
+import type {AdminConfig} from '@mattermost/types/config';
+
+import {ELASTICSEARCH_ALIAS, ELASTICSEARCH_PORT} from '../containers/constants';
+import {bootEnvMatches, restartMattermostContainer} from '../containers/stack';
+
+import {getAdminClient} from './init';
+
+import {testConfig} from '@/test_config';
+
+// The Mattermost server always connects to Elasticsearch itself (indexing/search requests), so it
+// needs the Testcontainers network alias, not a host-mapped address.
+export function elasticsearchServerConfig(): Partial {
+ return {
+ ConnectionURL: `http://${ELASTICSEARCH_ALIAS}:${ELASTICSEARCH_PORT}`,
+ EnableIndexing: true,
+ EnableSearching: true,
+ EnableAutocomplete: true,
+ Sniff: false,
+ };
+}
+
+/**
+ * Checks Elasticsearch was started this run, restarts the server onto it if the Elasticsearch Go
+ * client isn't already the registered search engine, and confirms the server can actually reach
+ * it — skipping the test otherwise, instead of failing on an unmet precondition. Backend picks
+ * which of two Go implementations (Elasticsearch vs OpenSearch client) the server registers — a
+ * factory invoked once at startup, never re-invoked by the config-change watcher — so switching it
+ * needs a restart, unlike the rest of ElasticsearchSettings, which the watcher does pick up live.
+ */
+export async function ensureElasticsearch(): Promise {
+ if (!testConfig.testcontainersServices.includes('elasticsearch')) {
+ test.skip(true, 'Skipping test - elasticsearch not started (set PW_TESTCONTAINERS_SERVICES=elasticsearch)');
+ return;
+ }
+
+ try {
+ const env = {MM_ELASTICSEARCHSETTINGS_BACKEND: 'elasticsearch'};
+ if (!bootEnvMatches(env)) {
+ await restartMattermostContainer(env);
+ }
+
+ const {adminClient} = await getAdminClient();
+ await adminClient.patchConfig({ElasticsearchSettings: elasticsearchServerConfig()});
+ await adminClient.testElasticsearch();
+ } catch (error) {
+ test.skip(true, `Skipping test - Elasticsearch connection test failed: ${String(error)}`);
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/server/email.ts b/e2e-tests/playwright/lib/src/server/email.ts
index 3491a3780d8a..dd39922ff4d1 100644
--- a/e2e-tests/playwright/lib/src/server/email.ts
+++ b/e2e-tests/playwright/lib/src/server/email.ts
@@ -47,6 +47,12 @@ export async function getRecentEmail(
throw new Error(`Timed out waiting for email to ${recipient}`);
}
+/**
+ * Extracts a link matching `pathname` from the email body and rewrites its origin to
+ * testConfig.baseURL. Links come back carrying ServiceSettings.SiteURL's origin, which in
+ * `testcontainers` mode is the Docker network alias the server uses to reach itself — not reachable from this
+ * test process or its browser.
+ */
export function extractEmailLink(email: InbucketEmail, pathname: string): string {
const escapedPathname = pathname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const link = email.body.text.match(new RegExp(`https?://[^\\s<>"')]+${escapedPathname}[^\\s<>"')]+`))?.[0];
@@ -55,7 +61,13 @@ export function extractEmailLink(email: InbucketEmail, pathname: string): string
throw new Error(`Email to ${email.to.join(', ')} does not contain a link for ${pathname}`);
}
- return link.replaceAll('&', '&');
+ const unescaped = link.replaceAll('&', '&');
+ const rewritten = new URL(unescaped);
+ const reachable = new URL(testConfig.baseURL);
+ rewritten.protocol = reachable.protocol;
+ rewritten.host = reachable.host;
+
+ return rewritten.toString();
}
/**
diff --git a/e2e-tests/playwright/lib/src/server/feature_flags.ts b/e2e-tests/playwright/lib/src/server/feature_flags.ts
new file mode 100644
index 000000000000..de95ddbd2a79
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/server/feature_flags.ts
@@ -0,0 +1,47 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@playwright/test';
+
+import {bootEnvMatches, restartMattermostContainer} from '../containers/stack';
+
+import {getAdminClient} from './init';
+
+import {testConfig} from '@/test_config';
+
+/**
+ * Restarts the server with the given feature flag set to `value` if it isn't already, and
+ * confirms the running server actually reports that value — skipping the test otherwise, instead
+ * of failing on an unmet precondition.
+ *
+ * FeatureFlags can't be changed via patchConfig on a running server at all: with no Split key
+ * configured (this test setup never sets one), the config store's readOnlyFF handling reverts any
+ * FeatureFlags patch back to its prior value before it's even persisted
+ * (server/config/store.go's Set()/Load()), so only a boot-time MM_FEATUREFLAGS_* env var actually
+ * takes effect.
+ */
+export async function ensureFeatureFlag(flagName: string, value: boolean): Promise {
+ if (!testConfig.useTestContainers) {
+ test.skip(true, 'Skipping test - feature flag restart requires PW_USE_TESTCONTAINERS=true');
+ return;
+ }
+
+ const envKey = `MM_FEATUREFLAGS_${flagName.toUpperCase()}`;
+ const envValue = String(value);
+
+ try {
+ const env = {[envKey]: envValue};
+ if (!bootEnvMatches(env)) {
+ await restartMattermostContainer(env);
+ }
+
+ const {adminClient} = await getAdminClient();
+ const config = await adminClient.getConfig();
+ const actual = config.FeatureFlags?.[flagName];
+ if (String(actual) !== envValue) {
+ throw new Error(`Feature flag "${flagName}" is "${String(actual)}" after restart, expected "${envValue}".`);
+ }
+ } catch (error) {
+ test.skip(true, `Skipping test - feature flag "${flagName}" check failed: ${String(error)}`);
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/server/filestore.ts b/e2e-tests/playwright/lib/src/server/filestore.ts
new file mode 100644
index 000000000000..60a15b676246
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/server/filestore.ts
@@ -0,0 +1,50 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@playwright/test';
+import type {Client4} from '@mattermost/client';
+import type {UserProfile} from '@mattermost/types/users';
+
+import {bootEnvMatches, restartMattermostContainer} from '../containers/stack';
+
+import {getAdminClient} from './init';
+
+import {testConfig} from '@/test_config';
+
+// A 1x1 transparent PNG, just to exercise a real write through the server's file backend.
+const PROBE_IMAGE_BASE64_PNG =
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=';
+
+/** Uploads a throwaway profile image, exercising the server's real, live file backend. */
+export async function uploadProbeImage(adminClient: Client4, adminUser: UserProfile | null): Promise {
+ if (!adminUser) {
+ throw new Error('No admin user available to probe a real upload with.');
+ }
+ const probeImage = new File([Buffer.from(PROBE_IMAGE_BASE64_PNG, 'base64')], 'probe.png', {type: 'image/png'});
+ await adminClient.uploadProfileImage(adminUser.id, probeImage);
+}
+
+/**
+ * Restarts the server onto local disk storage if it isn't already there, and confirms a real
+ * upload actually works — skipping the test otherwise, instead of failing on an unmet
+ * precondition. The counterpart to ensureMinio()/ensureAzurite() for specs that specifically need
+ * local storage active (e.g. after another spec in the same run switched it away).
+ */
+export async function ensureLocalFile(): Promise {
+ if (!testConfig.useTestContainers) {
+ test.skip(true, 'Skipping test - local file storage restart requires PW_USE_TESTCONTAINERS=true');
+ return;
+ }
+
+ try {
+ const env = {MM_FILESETTINGS_DRIVERNAME: 'local'};
+ if (!bootEnvMatches(env)) {
+ await restartMattermostContainer(env);
+ }
+
+ const {adminClient, adminUser} = await getAdminClient();
+ await uploadProbeImage(adminClient, adminUser);
+ } catch (error) {
+ test.skip(true, `Skipping test - local file storage check failed: ${String(error)}`);
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/server/index.ts b/e2e-tests/playwright/lib/src/server/index.ts
index b025c2127cdc..fcb21a471bda 100644
--- a/e2e-tests/playwright/lib/src/server/index.ts
+++ b/e2e-tests/playwright/lib/src/server/index.ts
@@ -38,3 +38,23 @@ export {
} from './abac_helpers';
export {installAndEnablePlugin, isPluginActive, getPluginStatus} from './plugin';
export {isWebhookTestServerReachable, setupWebhookTestServer} from './webhook_server';
+export {
+ generateLdapUser,
+ createLdapUser,
+ updateLdapUser,
+ deleteLdapUser,
+ ldapServerConfig,
+ ensureOpenldap,
+} from './openldap';
+export type {LdapUser} from './openldap';
+export {createKeycloakUser, deleteKeycloakUser, samlServerConfig, ensureKeycloak} from './keycloak';
+export type {KeycloakUser} from './keycloak';
+export {listMinioObjectKeys, ensureMinio} from './minio';
+export {elasticsearchServerConfig, ensureElasticsearch} from './elasticsearch';
+export {opensearchServerConfig, ensureOpensearch} from './opensearch';
+export {ensureAzurite, listAzuriteBlobNames} from './azurite';
+export {ensureLocalFile} from './filestore';
+export {ensurePostgresSearch} from './postgres_search';
+export {ensureFeatureFlag} from './feature_flags';
+export {runMmctl, ensureMmctl} from './mmctl';
+export type {MmctlResult} from './mmctl';
diff --git a/e2e-tests/playwright/lib/src/server/keycloak.ts b/e2e-tests/playwright/lib/src/server/keycloak.ts
new file mode 100644
index 000000000000..f1ed05d9f7d1
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/server/keycloak.ts
@@ -0,0 +1,151 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@playwright/test';
+import type {Client4} from '@mattermost/client';
+import type {AdminConfig} from '@mattermost/types/config';
+
+import {
+ KEYCLOAK_ADMIN_PASSWORD,
+ KEYCLOAK_ADMIN_USER,
+ KEYCLOAK_ALIAS,
+ KEYCLOAK_PORT,
+ KEYCLOAK_REALM,
+} from '../containers/constants';
+
+import {getAdminClient} from './init';
+
+import {testConfig} from '@/test_config';
+
+export type KeycloakUser = {
+ username: string;
+ email: string;
+ firstName: string;
+ lastName: string;
+ password: string;
+};
+
+async function getAdminToken(): Promise {
+ const response = await fetch(`${testConfig.keycloakUrl}/realms/master/protocol/openid-connect/token`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+ body: new URLSearchParams({
+ grant_type: 'password',
+ client_id: 'admin-cli',
+ username: KEYCLOAK_ADMIN_USER,
+ password: KEYCLOAK_ADMIN_PASSWORD,
+ }),
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to get Keycloak admin token: ${response.status} ${await response.text()}`);
+ }
+
+ const body = (await response.json()) as {access_token: string};
+ return body.access_token;
+}
+
+/** Creates the user in Keycloak and returns its Keycloak user id. */
+export async function createKeycloakUser(user: KeycloakUser): Promise {
+ const token = await getAdminToken();
+ const response = await fetch(`${testConfig.keycloakUrl}/admin/realms/${KEYCLOAK_REALM}/users`, {
+ method: 'POST',
+ headers: {Authorization: `Bearer ${token}`, 'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ username: user.username,
+ email: user.email,
+ firstName: user.firstName,
+ lastName: user.lastName,
+ enabled: true,
+ credentials: [{type: 'password', value: user.password, temporary: false}],
+ }),
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to create Keycloak user: ${response.status} ${await response.text()}`);
+ }
+
+ const location = response.headers.get('Location');
+ const userId = location?.split('/').pop();
+ if (!userId) {
+ throw new Error('Keycloak user creation response had no Location header to read the new user id from.');
+ }
+ return userId;
+}
+
+export async function deleteKeycloakUser(userId: string): Promise {
+ const token = await getAdminToken();
+ const response = await fetch(`${testConfig.keycloakUrl}/admin/realms/${KEYCLOAK_REALM}/users/${userId}`, {
+ method: 'DELETE',
+ headers: {Authorization: `Bearer ${token}`},
+ });
+ if (!response.ok && response.status !== 404) {
+ throw new Error(`Failed to delete Keycloak user: ${response.status} ${await response.text()}`);
+ }
+}
+
+// Matches the SAML client's clientId in keycloak-realm-export.json.
+const SAML_SERVICE_PROVIDER_ID = 'mattermost';
+
+// The Mattermost server fetches this URL itself (via POST /saml/metadatafromidp), so it must be
+// reachable from inside the Testcontainers network — unlike the IdpURL/IdpDescriptorURL below,
+// which the browser follows directly and so must be reachable from the host instead. Only the
+// certificate from this response is used; its embedded URLs reflect the alias host, not the one
+// the browser needs.
+function keycloakSamlDescriptorUrl(): string {
+ return `http://${KEYCLOAK_ALIAS}:${KEYCLOAK_PORT}/realms/${KEYCLOAK_REALM}/protocol/saml/descriptor`;
+}
+
+// The metadata response's certificate is the raw base64 DER content straight out of the SAML
+// metadata XML's element — no PEM armor — but IdpCertificateFile parsing
+// requires a proper PEM block.
+function toPemCertificate(base64Der: string): string {
+ const lines = base64Der.replace(/\s+/g, '').match(/.{1,64}/g) ?? [];
+ return `-----BEGIN CERTIFICATE-----\n${lines.join('\n')}\n-----END CERTIFICATE-----\n`;
+}
+
+/**
+ * Fetches Keycloak's SAML IdP certificate (via the server's own metadata-from-IdP call) and
+ * uploads it, then returns a `SamlSettings` patch pointing the server at Keycloak's SAML IdP and
+ * the users `createKeycloakUser` creates.
+ */
+export async function samlServerConfig(adminClient: Client4): Promise> {
+ const metadata = await adminClient.getSamlMetadataFromIdp(keycloakSamlDescriptorUrl());
+ const certificate = toPemCertificate(metadata.idp_public_certificate);
+ await adminClient.uploadIdpSamlCertificate(new File([certificate], 'idp-certificate.crt'));
+
+ return {
+ Enable: true,
+ Verify: true,
+ Encrypt: false,
+ SignRequest: false,
+ IdpURL: `${testConfig.keycloakUrl}/realms/${KEYCLOAK_REALM}/protocol/saml`,
+ IdpDescriptorURL: `${testConfig.keycloakUrl}/realms/${KEYCLOAK_REALM}`,
+ ServiceProviderIdentifier: SAML_SERVICE_PROVIDER_ID,
+ AssertionConsumerServiceURL: `${testConfig.baseURL}/login/sso/saml`,
+ IdAttribute: 'id',
+ EmailAttribute: 'email',
+ UsernameAttribute: 'username',
+ FirstNameAttribute: 'givenName',
+ LastNameAttribute: 'surname',
+ LoginButtonText: 'Keycloak SAML',
+ };
+}
+
+/**
+ * Checks Keycloak was started this run, points the server's SAML settings at it (fetching its IdP
+ * metadata/certificate along the way), and skips the test if either step fails, instead of
+ * failing on an unmet precondition.
+ */
+export async function ensureKeycloak(): Promise {
+ if (!testConfig.testcontainersServices.includes('keycloak')) {
+ test.skip(true, 'Skipping test - keycloak not started (set PW_TESTCONTAINERS_SERVICES=keycloak)');
+ return;
+ }
+
+ try {
+ const {adminClient} = await getAdminClient();
+ const config = await samlServerConfig(adminClient);
+ await adminClient.patchConfig({SamlSettings: config});
+ } catch (error) {
+ test.skip(true, `Skipping test - Keycloak SAML setup failed: ${String(error)}`);
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/server/minio.ts b/e2e-tests/playwright/lib/src/server/minio.ts
new file mode 100644
index 000000000000..319c1e08f629
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/server/minio.ts
@@ -0,0 +1,93 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {Client as MinioClient} from 'minio';
+import {test} from '@playwright/test';
+
+import {MINIO_ACCESS_KEY, MINIO_ALIAS, MINIO_BUCKET, MINIO_PORT, MINIO_SECRET_KEY} from '../containers/constants';
+import {bootEnvMatches, restartMattermostContainer} from '../containers/stack';
+
+import {uploadProbeImage} from './filestore';
+import {getAdminClient} from './init';
+
+import {testConfig} from '@/test_config';
+
+function getMinioClient(): MinioClient {
+ const url = new URL(testConfig.minioUrl);
+ return new MinioClient({
+ endPoint: url.hostname,
+ port: Number(url.port),
+ useSSL: url.protocol === 'https:',
+ accessKey: MINIO_ACCESS_KEY,
+ secretKey: MINIO_SECRET_KEY,
+ });
+}
+
+// Only used internally by ensureMinio() — not a spec-facing entry point.
+async function ensureMinioBucket(bucket: string = MINIO_BUCKET): Promise {
+ const client = getMinioClient();
+ const exists = await client.bucketExists(bucket);
+ if (!exists) {
+ await client.makeBucket(bucket);
+ }
+}
+
+/** Lists every object key in the bucket, to confirm the server actually wrote to Minio. */
+export async function listMinioObjectKeys(bucket: string = MINIO_BUCKET): Promise {
+ const client = getMinioClient();
+ const keys: string[] = [];
+ for await (const item of client.listObjectsV2(bucket, '', true)) {
+ if (item.name) {
+ keys.push(item.name);
+ }
+ }
+ return keys;
+}
+
+// FileSettings' backend is chosen once when the Mattermost process boots and never re-read from a
+// running server's config, so pointing the server at Minio can only happen via the env vars the
+// container starts with — always via the network alias, since the server itself always runs
+// inside the Testcontainers network.
+function minioServerEnv(): Record {
+ return {
+ MM_FILESETTINGS_DRIVERNAME: 'amazons3',
+ MM_FILESETTINGS_AMAZONS3ENDPOINT: `${MINIO_ALIAS}:${MINIO_PORT}`,
+ MM_FILESETTINGS_AMAZONS3ACCESSKEYID: MINIO_ACCESS_KEY,
+ MM_FILESETTINGS_AMAZONS3SECRETACCESSKEY: MINIO_SECRET_KEY,
+ MM_FILESETTINGS_AMAZONS3BUCKET: MINIO_BUCKET,
+ MM_FILESETTINGS_AMAZONS3SSL: 'false',
+ };
+}
+
+/**
+ * Checks Minio was started this run, restarts the server onto it if it isn't already the active
+ * file storage backend, and confirms a real upload actually lands in Minio — skipping the test
+ * otherwise, instead of failing on an unmet precondition.
+ */
+export async function ensureMinio(): Promise {
+ if (!testConfig.testcontainersServices.includes('minio')) {
+ test.skip(true, 'Skipping test - minio not started (set PW_TESTCONTAINERS_SERVICES=minio)');
+ return;
+ }
+
+ try {
+ await ensureMinioBucket();
+ const env = minioServerEnv();
+ if (!bootEnvMatches(env)) {
+ await restartMattermostContainer(env);
+ }
+
+ const {adminClient, adminUser} = await getAdminClient();
+ await uploadProbeImage(adminClient, adminUser);
+
+ const objectKeys = await listMinioObjectKeys();
+ if (objectKeys.length === 0) {
+ throw new Error(
+ 'Minio bucket is still empty after a real upload — the server is not actually using Minio ' +
+ 'as its file backend.',
+ );
+ }
+ } catch (error) {
+ test.skip(true, `Skipping test - Minio connection test failed: ${String(error)}`);
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/server/mmctl.ts b/e2e-tests/playwright/lib/src/server/mmctl.ts
new file mode 100644
index 000000000000..e6888ab9522d
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/server/mmctl.ts
@@ -0,0 +1,46 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@playwright/test';
+
+import {runMmctl as runMmctlContainer} from '../containers/mmctl_container';
+import type {MmctlResult} from '../containers/mmctl_container';
+
+import {getAdminClient} from './init';
+
+import {testConfig} from '@/test_config';
+
+export type {MmctlResult};
+
+/**
+ * Runs an mmctl command as a real remote client: a separate container built from the same server
+ * image, authenticated with the current admin session, reaching the server over the
+ * Testcontainers network rather than the `--local` unix socket the server's own container uses.
+ */
+export async function runMmctl(args: string[]): Promise {
+ const {adminClient, adminUser} = await getAdminClient();
+ if (!adminUser) {
+ throw new Error('No admin user available to authenticate mmctl with.');
+ }
+ return runMmctlContainer(args, adminUser.username, adminClient.getToken());
+}
+
+/**
+ * Checks full (Testcontainers) mode is active and a real remote mmctl invocation actually reaches
+ * the server — skipping the test otherwise, instead of failing on an unmet precondition.
+ */
+export async function ensureMmctl(): Promise {
+ if (!testConfig.useTestContainers) {
+ test.skip(true, 'Skipping test - remote mmctl container requires PW_USE_TESTCONTAINERS=true');
+ return;
+ }
+
+ try {
+ const result = await runMmctl(['version']);
+ if (result.exitCode !== 0) {
+ throw new Error(`mmctl exited with code ${result.exitCode}: ${result.output}`);
+ }
+ } catch (error) {
+ test.skip(true, `Skipping test - mmctl connectivity check failed: ${String(error)}`);
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/server/openldap.ts b/e2e-tests/playwright/lib/src/server/openldap.ts
new file mode 100644
index 000000000000..13e27bb1e828
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/server/openldap.ts
@@ -0,0 +1,162 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {AlreadyExistsError, Attribute, Change, Client} from 'ldapts';
+import {test} from '@playwright/test';
+import type {AdminConfig} from '@mattermost/types/config';
+
+import {
+ OPENLDAP_ADMIN_DN,
+ OPENLDAP_ADMIN_PASSWORD,
+ OPENLDAP_ALIAS,
+ OPENLDAP_BASE_DN,
+ OPENLDAP_PORT,
+} from '../containers/constants';
+
+import {getAdminClient} from './init';
+
+import {testConfig} from '@/test_config';
+import {getRandomId} from '@/util';
+
+// Typed in-process LDAP client — avoids depending on external ldapadd/ldapmodify binaries.
+
+export type LdapUser = {
+ username: string;
+ password: string;
+ email: string;
+ firstname: string;
+ lastname: string;
+};
+
+const ORG_UNIT = 'e2etest';
+const ORG_UNIT_DN = `ou=${ORG_UNIT},${OPENLDAP_BASE_DN}`;
+
+export function generateLdapUser(prefix = 'ldap'): LdapUser {
+ const randomId = getRandomId();
+ const username = `${prefix}user${randomId}`;
+ return {
+ username,
+ password: 'Password1',
+ email: `${username}@mmtest.com`,
+ firstname: `Firstname-${randomId}`,
+ lastname: `Lastname-${randomId}`,
+ };
+}
+
+async function withAdminClient(fn: (client: Client) => Promise): Promise {
+ const client = new Client({url: `ldap://${testConfig.ldapHost}:${testConfig.ldapPort}`});
+ try {
+ await client.bind(OPENLDAP_ADMIN_DN, OPENLDAP_ADMIN_PASSWORD);
+ return await fn(client);
+ } finally {
+ await client.unbind();
+ }
+}
+
+async function ensureOrgUnit(client: Client): Promise {
+ try {
+ await client.add(ORG_UNIT_DN, {objectClass: 'organizationalUnit', ou: ORG_UNIT});
+ } catch (error) {
+ if (!(error instanceof AlreadyExistsError)) {
+ throw error;
+ }
+ }
+}
+
+/** Creates the user (and the shared org unit, if it doesn't exist yet) and returns it. */
+export async function createLdapUser(user: LdapUser = generateLdapUser()): Promise {
+ await withAdminClient(async (client) => {
+ await ensureOrgUnit(client);
+ await client.add(`uid=${user.username},${ORG_UNIT_DN}`, {
+ objectClass: 'inetOrgPerson',
+ cn: user.firstname,
+ sn: user.lastname,
+ uid: user.username,
+ mail: user.email,
+ userPassword: user.password,
+ });
+ });
+ return user;
+}
+
+export async function updateLdapUser(username: string, changes: Partial>): Promise {
+ await withAdminClient(async (client) => {
+ const dn = `uid=${username},${ORG_UNIT_DN}`;
+ const attributeByType: Array<[string, string | undefined]> = [
+ ['cn', changes.firstname],
+ ['sn', changes.lastname],
+ ['mail', changes.email],
+ ['userPassword', changes.password],
+ ];
+
+ const modifications = attributeByType
+ .filter((entry): entry is [string, string] => entry[1] !== undefined)
+ .map(
+ ([type, value]) =>
+ new Change({operation: 'replace', modification: new Attribute({type, values: [value]})}),
+ );
+
+ if (modifications.length > 0) {
+ await client.modify(dn, modifications);
+ }
+ });
+}
+
+export async function deleteLdapUser(username: string): Promise {
+ await withAdminClient(async (client) => {
+ await client.del(`uid=${username},${ORG_UNIT_DN}`);
+ });
+}
+
+// In `testcontainers` mode the Mattermost server is itself a container on the Testcontainers network, so it
+// must reach OpenLDAP via its network alias — unlike testConfig.ldapHost/ldapPort, which resolve to
+// a host-mapped address for this module's own ldapts client (a separate, non-containerized process).
+function ldapServerAddress(): [string, number] {
+ return testConfig.useTestContainers ? [OPENLDAP_ALIAS, OPENLDAP_PORT] : [testConfig.ldapHost, testConfig.ldapPort];
+}
+
+/** `LdapSettings` patch pointing the server at the OpenLDAP container and the users this module creates. */
+export function ldapServerConfig(): Partial {
+ const [ldapServer, ldapPort] = ldapServerAddress();
+ return {
+ Enable: true,
+ LdapServer: ldapServer,
+ LdapPort: ldapPort,
+ BaseDN: ORG_UNIT_DN,
+ BindUsername: OPENLDAP_ADMIN_DN,
+ BindPassword: OPENLDAP_ADMIN_PASSWORD,
+ UserFilter: '(objectClass=inetOrgPerson)',
+ IdAttribute: 'uid',
+ LoginIdAttribute: 'uid',
+ UsernameAttribute: 'uid',
+ EmailAttribute: 'mail',
+ FirstNameAttribute: 'cn',
+ LastNameAttribute: 'sn',
+ };
+}
+
+/**
+ * Checks OpenLDAP was started this run, points the server at it, and confirms the server can
+ * actually reach it — skipping the test otherwise, instead of failing on an unmet precondition.
+ */
+export async function ensureOpenldap(): Promise {
+ if (!testConfig.testcontainersServices.includes('openldap')) {
+ test.skip(true, 'Skipping test - openldap not started (set PW_TESTCONTAINERS_SERVICES=openldap)');
+ return;
+ }
+
+ const {adminClient} = await getAdminClient();
+ await adminClient.patchConfig({LdapSettings: ldapServerConfig()});
+
+ // testLdap() searches under BaseDN and requires at least one matching user to succeed, so
+ // probe with a throwaway user rather than depending on whatever the spec creates afterward.
+ const probeUser = generateLdapUser('ensureprobe');
+ try {
+ await createLdapUser(probeUser);
+ await adminClient.testLdap();
+ } catch (error) {
+ test.skip(true, `Skipping test - LDAP connection test failed: ${String(error)}`);
+ } finally {
+ await deleteLdapUser(probeUser.username).catch(() => undefined);
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/server/opensearch.ts b/e2e-tests/playwright/lib/src/server/opensearch.ts
new file mode 100644
index 000000000000..016b3965f085
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/server/opensearch.ts
@@ -0,0 +1,54 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@playwright/test';
+import type {AdminConfig} from '@mattermost/types/config';
+
+import {OPENSEARCH_ADMIN_PASSWORD, OPENSEARCH_ALIAS, OPENSEARCH_PORT} from '../containers/constants';
+import {bootEnvMatches, restartMattermostContainer} from '../containers/stack';
+
+import {getAdminClient} from './init';
+
+import {testConfig} from '@/test_config';
+
+// The Mattermost server always connects to OpenSearch itself (indexing/search requests), so it
+// needs the Testcontainers network alias, not a host-mapped address.
+export function opensearchServerConfig(): Partial {
+ return {
+ ConnectionURL: `http://${OPENSEARCH_ALIAS}:${OPENSEARCH_PORT}`,
+ Username: 'admin',
+ Password: OPENSEARCH_ADMIN_PASSWORD,
+ EnableIndexing: true,
+ EnableSearching: true,
+ EnableAutocomplete: true,
+ Sniff: false,
+ };
+}
+
+/**
+ * Checks OpenSearch was started this run, restarts the server onto it if the OpenSearch Go client
+ * isn't already the registered search engine, and confirms the server can actually reach it —
+ * skipping the test otherwise, instead of failing on an unmet precondition. Backend picks which of
+ * two Go implementations (Elasticsearch vs OpenSearch client) the server registers — a factory
+ * invoked once at startup, never re-invoked by the config-change watcher — so switching it needs a
+ * restart, unlike the rest of ElasticsearchSettings, which the watcher does pick up live.
+ */
+export async function ensureOpensearch(): Promise {
+ if (!testConfig.testcontainersServices.includes('opensearch')) {
+ test.skip(true, 'Skipping test - opensearch not started (set PW_TESTCONTAINERS_SERVICES=opensearch)');
+ return;
+ }
+
+ try {
+ const env = {MM_ELASTICSEARCHSETTINGS_BACKEND: 'opensearch'};
+ if (!bootEnvMatches(env)) {
+ await restartMattermostContainer(env);
+ }
+
+ const {adminClient} = await getAdminClient();
+ await adminClient.patchConfig({ElasticsearchSettings: opensearchServerConfig()});
+ await adminClient.testElasticsearch();
+ } catch (error) {
+ test.skip(true, `Skipping test - OpenSearch connection test failed: ${String(error)}`);
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/server/postgres_search.ts b/e2e-tests/playwright/lib/src/server/postgres_search.ts
new file mode 100644
index 000000000000..fa578346dc77
--- /dev/null
+++ b/e2e-tests/playwright/lib/src/server/postgres_search.ts
@@ -0,0 +1,27 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@playwright/test';
+
+import {getAdminClient} from './init';
+
+/**
+ * Disables Elasticsearch/OpenSearch indexing/searching so search falls back to the database —
+ * skipping the test otherwise, instead of failing on an unmet precondition. The counterpart to
+ * ensureElasticsearch()/ensureOpensearch() for specs that specifically need database-backed search
+ * active (e.g. after another spec in the same run switched it away).
+ *
+ * No restart involved, unlike most other ensure*() functions: which search engine (if any) is
+ * active is decided at runtime from these enable flags, not from the boot-time Backend setting, so
+ * disabling them here is enough to make the server fall through to Postgres-backed search.
+ */
+export async function ensurePostgresSearch(): Promise {
+ try {
+ const {adminClient} = await getAdminClient();
+ await adminClient.patchConfig({
+ ElasticsearchSettings: {EnableIndexing: false, EnableSearching: false, EnableAutocomplete: false},
+ });
+ } catch (error) {
+ test.skip(true, `Skipping test - database search check failed: ${String(error)}`);
+ }
+}
diff --git a/e2e-tests/playwright/lib/src/test_config.ts b/e2e-tests/playwright/lib/src/test_config.ts
index 210301d95e3f..821018a451ce 100644
--- a/e2e-tests/playwright/lib/src/test_config.ts
+++ b/e2e-tests/playwright/lib/src/test_config.ts
@@ -3,7 +3,28 @@
import * as dotenv from 'dotenv';
+import {MATTERMOST_ALIAS, MATTERMOST_PORT, WEBHOOK_ALIAS, WEBHOOK_PORT} from './containers/constants';
+import {MATTERMOST_SERVER_IMAGE} from './containers/default_images';
+import {SERVER_ENV_BASELINE} from './containers/env_baseline';
+
dotenv.config({quiet: true});
+dotenv.config({path: '.env.testcontainers', quiet: true, override: true});
+
+// The set of additional services `testcontainers` mode knows how to start.
+// Single source of truth for `requirements.ts` and for validating PW_TESTCONTAINERS_SERVICES.
+export const TESTCONTAINERS_SERVICE_NAMES = [
+ 'openldap',
+ 'keycloak',
+ 'elasticsearch',
+ 'opensearch',
+ 'minio',
+ 'azurite',
+] as const;
+export type TestContainersServiceName = (typeof TESTCONTAINERS_SERVICE_NAMES)[number];
+
+// Started whenever PW_TESTCONTAINERS_SERVICES is unset
+// opensearch and azurite are opt-in only
+const DEFAULT_TESTCONTAINERS_SERVICES: TestContainersServiceName[] = ['minio', 'openldap', 'keycloak', 'elasticsearch'];
// All process.env should be defined here
export class TestConfig {
@@ -24,9 +45,85 @@ export class TestConfig {
snapshotEnabled: boolean;
percyEnabled: boolean;
smtpURL: string;
+ /**
+ * Postgres connection string for specs needing direct DB access to bypass API-level
+ * validation. In `testcontainers` mode the default port is a placeholder — startStack() overwrites
+ * it with the actual Testcontainers-assigned host-mapped port once the container is up.
+ */
+ postgresUrl: string;
/** Base URL of the Cypress/Playwright webhook sidecar (`e2e-tests/cypress`: `npm run start:webhook`). */
webhookBaseUrl: string;
+ /**
+ * How OTHER containers (not the test process) reach Mattermost — used when a URL is embedded
+ * in a request body the server itself later dereferences (e.g. a webhook callback URL). In
+ * `external` mode this equals baseURL. In `testcontainers` mode it's the fixed Docker network alias,
+ * since a container can't reach another container's host-mapped port via `localhost`.
+ */
+ internalBaseURL: string;
+ /** Same distinction as internalBaseURL, for the webhook sidecar's own address. */
+ webhookInternalUrl: string;
+ /**
+ * Gateway IP (e.g. 172.18.0.1) of the Testcontainers bridge network in `testcontainers` mode — an
+ * address on the Docker host itself, reachable both from the browser and from inside the
+ * Mattermost container, so a single mock file server URL works for both without a network
+ * alias or mapped port. Empty in `external` mode, where plain localhost already works for
+ * both since neither runs in a container.
+ */
+ testcontainersNetworkGatewayIp: string;
+
+ // Testcontainers (`testcontainers` mode)
+
+ /** Selects `testcontainers` mode (Testcontainers manages the server + dependencies) over `external` mode (default). */
+ useTestContainers: boolean;
+ /** Prebuilt Mattermost server image `testcontainers` mode starts. Same env var Compose-based CI already uses. */
+ serverImage: string;
+ /** Arbitrary MM_* config overrides merged over the test-oriented baseline, comma-separated KEY=VALUE pairs. */
+ serverEnv: Record;
+ /**
+ * Which additional services `testcontainers` mode starts, validated against
+ * `TESTCONTAINERS_SERVICE_NAMES`. Defaults to `DEFAULT_TESTCONTAINERS_SERVICES` when
+ * PW_TESTCONTAINERS_SERVICES is unset; set it to an empty string to start none.
+ */
+ testcontainersServices: TestContainersServiceName[];
+ /** Reuse containers across repeated local `testcontainers`-mode runs instead of recreating them every time. */
+ testcontainersReuse: boolean;
+ /** Playwright itself runs inside a container (e.g. CI); join it to the Testcontainers network instead of using mapped ports. */
+ containerRunner: boolean;
+ /**
+ * Name of the Docker network the stack runs on. getNetwork()'s in-process cache only helps
+ * within the process that called startStack() (global setup) — worker processes need this to
+ * join a container (e.g. the mmctl runner) to that same network instead of creating their own.
+ */
+ testcontainersNetworkName: string;
+ /**
+ * ID of the currently-running Mattermost container. Global setup and worker processes are
+ * separate OS processes — this is how a worker's restartMattermostContainer() (e.g. to switch
+ * file storage backends) finds the container to replace without an in-process object handle.
+ */
+ mattermostContainerId: string;
+ /**
+ * The full set of env vars the current Mattermost container actually booted with — baseline
+ * plus every boot-time-only setting a pw.ensure*() has switched via
+ * restartMattermostContainer() so far. Each ensure*() checks this before restarting, so it
+ * only restarts when something actually needs to change, and restartMattermostContainer()
+ * merges into this record rather than replacing it, so a restart for one setting doesn't
+ * undo another restart's change.
+ *
+ * Persisted to .env.testcontainers and read back rather than recomputed, because a container
+ * can outlive the process that booted it (e.g. reused across specs in CI) — a later process
+ * must see the server's actual current state, not assume defaults.
+ */
+ bootEnvOverrides: Record;
+
+ /** Used in every mode; defaults match the fixed ports the Testcontainers stack publishes. */
+ ldapHost: string;
+ ldapPort: number;
+ keycloakUrl: string;
+ elasticsearchUrl: string;
+ opensearchUrl: string;
+ minioUrl: string;
+ azuriteUrl: string;
constructor() {
// Server
@@ -54,7 +151,42 @@ export class TestConfig {
this.percyEnabled = parseBool(process.env.PW_PERCY_ENABLE, false);
// Email
this.smtpURL = process.env.PW_SMTP_URL || 'http://localhost:9001';
+ this.postgresUrl =
+ process.env.PW_POSTGRES_URL ||
+ 'postgres://mmuser:mostest@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10&binary_parameters=yes';
this.webhookBaseUrl = process.env.PW_WEBHOOK_BASE_URL || 'http://localhost:3000';
+
+ // Testcontainers
+ this.useTestContainers = parseBool(process.env.PW_USE_TESTCONTAINERS, false);
+ // Fixed Docker network aliases, not derived from any started container object — stable
+ // across a restartMattermostContainer() call, unlike baseURL's host-mapped port.
+ this.internalBaseURL = this.useTestContainers ? `http://${MATTERMOST_ALIAS}:${MATTERMOST_PORT}` : this.baseURL;
+ this.webhookInternalUrl = this.useTestContainers
+ ? `http://${WEBHOOK_ALIAS}:${WEBHOOK_PORT}`
+ : this.webhookBaseUrl;
+ this.testcontainersNetworkGatewayIp = process.env.PW_TESTCONTAINERS_NETWORK_GATEWAY_IP || '';
+ this.serverImage = process.env.SERVER_IMAGE || MATTERMOST_SERVER_IMAGE;
+ this.serverEnv = parseKeyValueList(process.env.MM_ENV);
+ this.testcontainersServices = parseTestContainersServices(process.env.PW_TESTCONTAINERS_SERVICES);
+ // Defaults to true so a local Ctrl+C (or just finishing a run) doesn't throw away the
+ // stack you were likely about to inspect — tear down explicitly with `npm run testcontainers:down`.
+ this.testcontainersReuse = parseBool(process.env.PW_TESTCONTAINERS_REUSE, true);
+ this.containerRunner = parseBool(process.env.PW_TESTCONTAINERS_CONTAINER_RUNNER, false);
+ this.testcontainersNetworkName = process.env.PW_TESTCONTAINERS_NETWORK_NAME || '';
+ this.mattermostContainerId = process.env.PW_TESTCONTAINERS_MATTERMOST_CONTAINER_ID || '';
+ // Prefer a previously-persisted boot env, since it reflects the container's actual
+ // current state; only compute the default when it's genuinely absent (first boot). A
+ // stale file pointing at a container that's no longer running is detected and reset
+ // elsewhere, before a fresh container starts.
+ this.bootEnvOverrides =
+ parsePersistedBootEnv(process.env.PW_TESTCONTAINERS_BOOT_ENV) ?? computeDefaultBootEnv(this.serverEnv);
+ this.ldapHost = process.env.PW_LDAP_HOST || 'localhost';
+ this.ldapPort = parseNumber(process.env.PW_LDAP_PORT, 389);
+ this.keycloakUrl = process.env.PW_KEYCLOAK_URL || 'http://localhost:8484';
+ this.elasticsearchUrl = process.env.PW_ELASTICSEARCH_URL || 'http://localhost:9200';
+ this.opensearchUrl = process.env.PW_OPENSEARCH_URL || 'http://localhost:9201';
+ this.minioUrl = process.env.PW_MINIO_URL || 'http://localhost:9000';
+ this.azuriteUrl = process.env.PW_AZURITE_URL || 'http://localhost:10000';
}
}
@@ -68,3 +200,75 @@ function parseBool(actualValue: string | undefined, defaultValue: boolean) {
function parseNumber(actualValue: string | undefined, defaultValue: number) {
return actualValue ? parseInt(actualValue, 10) : defaultValue;
}
+
+function parseKeyValueList(actualValue: string | undefined): Record {
+ if (!actualValue) {
+ return {};
+ }
+
+ const result: Record = {};
+ for (const entry of actualValue
+ .split(',')
+ .map((part) => part.trim())
+ .filter(Boolean)) {
+ const separatorIndex = entry.indexOf('=');
+ if (separatorIndex === -1) {
+ throw new Error(`Invalid MM_ENV entry "${entry}" — expected KEY=VALUE.`);
+ }
+ result[entry.slice(0, separatorIndex)] = entry.slice(separatorIndex + 1);
+ }
+ return result;
+}
+
+// Mirrors startMattermostContainer()'s actual default boot env: baseline plus MM_ENV overrides
+// (same precedence), plus the two settings it never sets itself, so the server falls back to
+// its own defaults for those.
+function computeDefaultBootEnv(serverEnv: Record): Record {
+ return {
+ ...SERVER_ENV_BASELINE,
+ MM_FILESETTINGS_DRIVERNAME: 'local',
+ MM_ELASTICSEARCHSETTINGS_BACKEND: 'elasticsearch',
+ ...serverEnv,
+ };
+}
+
+/**
+ * Boot env a genuinely fresh container starts with. Used to reset bootEnvOverrides when a
+ * persisted boot env turns out to reference a container that's no longer running.
+ */
+export function defaultBootEnv(): Record {
+ return computeDefaultBootEnv(testConfig.serverEnv);
+}
+
+function parsePersistedBootEnv(actualValue: string | undefined): Record | undefined {
+ if (!actualValue) {
+ return undefined;
+ }
+ try {
+ return JSON.parse(actualValue);
+ } catch {
+ return undefined;
+ }
+}
+
+function parseTestContainersServices(actualValue: string | undefined): TestContainersServiceName[] {
+ if (actualValue === undefined) {
+ return DEFAULT_TESTCONTAINERS_SERVICES;
+ }
+
+ // An explicit empty string is a deliberate opt-out (e.g. a spec that wants only the core
+ // stack), distinct from not setting the var at all.
+ if (!actualValue) {
+ return [];
+ }
+
+ const requested = actualValue.split(',').filter(Boolean);
+ const unknown = requested.filter((name) => !(TESTCONTAINERS_SERVICE_NAMES as readonly string[]).includes(name));
+ if (unknown.length > 0) {
+ throw new Error(
+ `Unknown PW_TESTCONTAINERS_SERVICES entr${unknown.length > 1 ? 'ies' : 'y'}: ${unknown.join(', ')}. ` +
+ `Valid services are: ${TESTCONTAINERS_SERVICE_NAMES.join(', ')}.`,
+ );
+ }
+ return requested as TestContainersServiceName[];
+}
diff --git a/e2e-tests/playwright/lib/src/test_fixture.ts b/e2e-tests/playwright/lib/src/test_fixture.ts
index 47094a77128e..659a50dd1cb0 100644
--- a/e2e-tests/playwright/lib/src/test_fixture.ts
+++ b/e2e-tests/playwright/lib/src/test_fixture.ts
@@ -20,6 +20,8 @@ import {
import {getBlobFromAsset, getFileFromAsset} from './file';
import {
configureAIBridgeMock,
+ createKeycloakUser,
+ createLdapUser,
createMockAIAgent,
createNewUserProfile,
createNewTeam,
@@ -28,11 +30,23 @@ import {
createRandomTeam,
createRandomUser,
createUserWithAttributes,
+ deleteKeycloakUser,
+ deleteLdapUser,
enableAIBridgeTestMode,
+ listMinioObjectKeys,
+ ensureMinio,
+ ensureAzurite,
+ listAzuriteBlobNames,
+ ensureLocalFile,
+ ensurePostgresSearch,
+ ensureFeatureFlag,
+ generateLdapUser,
getAIBridgeMock,
getAdminClient,
initSetup,
isOutsideRemoteUserHour,
+ ldapServerConfig,
+ ensureOpenldap,
makeClient,
mergeWithOnPremServerConfig,
recapCompletion,
@@ -40,6 +54,15 @@ import {
rewriteCompletion,
installAndEnablePlugin,
isPluginActive,
+ samlServerConfig,
+ ensureKeycloak,
+ elasticsearchServerConfig,
+ opensearchServerConfig,
+ ensureElasticsearch,
+ ensureOpensearch,
+ runMmctl,
+ ensureMmctl,
+ updateLdapUser,
} from './server';
import {
toBeFocusedWithFocusVisible,
@@ -74,7 +97,6 @@ type AxeBuilderOptions = {
};
export const test = base.extend({
- // eslint-disable-next-line no-empty-pattern
axe: async ({}, use) => {
const ab = new AxeBuilderExtended();
await use(ab);
@@ -118,6 +140,31 @@ export class PlaywrightExtended {
readonly installAndEnablePlugin;
readonly isPluginActive;
+ // ./server/openldap, ./server/keycloak, ./server/elasticsearch, ./server/opensearch, ./server/minio
+ readonly generateLdapUser;
+ readonly createLdapUser;
+ readonly updateLdapUser;
+ readonly deleteLdapUser;
+ readonly ldapServerConfig;
+ readonly ensureOpenldap;
+ readonly createKeycloakUser;
+ readonly deleteKeycloakUser;
+ readonly samlServerConfig;
+ readonly ensureKeycloak;
+ readonly elasticsearchServerConfig;
+ readonly opensearchServerConfig;
+ readonly ensureElasticsearch;
+ readonly ensureOpensearch;
+ readonly listMinioObjectKeys;
+ readonly ensureMinio;
+ readonly ensureAzurite;
+ readonly ensureLocalFile;
+ readonly ensurePostgresSearch;
+ readonly ensureFeatureFlag;
+ readonly listAzuriteBlobNames;
+ readonly runMmctl;
+ readonly ensureMmctl;
+
// ./test_action
readonly toBeFocusedWithFocusVisible;
readonly hideDynamicChannelsContent;
@@ -193,6 +240,31 @@ export class PlaywrightExtended {
this.installAndEnablePlugin = installAndEnablePlugin;
this.isPluginActive = isPluginActive;
+ // ./server/openldap, ./server/keycloak, ./server/elasticsearch, ./server/opensearch, ./server/minio
+ this.generateLdapUser = generateLdapUser;
+ this.createLdapUser = createLdapUser;
+ this.updateLdapUser = updateLdapUser;
+ this.deleteLdapUser = deleteLdapUser;
+ this.ldapServerConfig = ldapServerConfig;
+ this.ensureOpenldap = ensureOpenldap;
+ this.createKeycloakUser = createKeycloakUser;
+ this.deleteKeycloakUser = deleteKeycloakUser;
+ this.samlServerConfig = samlServerConfig;
+ this.ensureKeycloak = ensureKeycloak;
+ this.elasticsearchServerConfig = elasticsearchServerConfig;
+ this.opensearchServerConfig = opensearchServerConfig;
+ this.ensureElasticsearch = ensureElasticsearch;
+ this.ensureOpensearch = ensureOpensearch;
+ this.listMinioObjectKeys = listMinioObjectKeys;
+ this.ensureMinio = ensureMinio;
+ this.ensureAzurite = ensureAzurite;
+ this.ensureLocalFile = ensureLocalFile;
+ this.ensurePostgresSearch = ensurePostgresSearch;
+ this.ensureFeatureFlag = ensureFeatureFlag;
+ this.listAzuriteBlobNames = listAzuriteBlobNames;
+ this.runMmctl = runMmctl;
+ this.ensureMmctl = ensureMmctl;
+
// ./test_action
this.toBeFocusedWithFocusVisible = toBeFocusedWithFocusVisible;
this.hideDynamicChannelsContent = hideDynamicChannelsContent;
diff --git a/e2e-tests/playwright/lib/src/ui/pages/login.ts b/e2e-tests/playwright/lib/src/ui/pages/login.ts
index 565c951cf426..5d356e52323e 100644
--- a/e2e-tests/playwright/lib/src/ui/pages/login.ts
+++ b/e2e-tests/playwright/lib/src/ui/pages/login.ts
@@ -16,6 +16,7 @@ export default class LoginPage {
readonly loginInput;
readonly loginPlaceholder;
readonly loginWithAdLdapPlaceholder;
+ readonly samlLoginButton;
readonly passwordInput;
readonly passwordToggleButton;
readonly signInButton;
@@ -34,7 +35,8 @@ export default class LoginPage {
this.bodyCard = page.getByTestId('login-body-card');
this.loginInput = page.locator('#input_loginId');
this.loginPlaceholder = page.getByPlaceholder('Email or Username');
- this.loginWithAdLdapPlaceholder = page.getByPlaceholder('Email, Username or AD/LDAP Username');
+ this.loginWithAdLdapPlaceholder = page.getByRole('textbox', {name: 'Email, Username or AD/LDAP Username'});
+ this.samlLoginButton = page.locator('#saml');
this.passwordInput = page.locator('#input_password-input');
this.passwordToggleButton = page.locator('#password_toggle');
this.signInButton = page.getByRole('button', {name: 'Log in'});
diff --git a/e2e-tests/playwright/mock_file_server.js b/e2e-tests/playwright/mock_file_server.js
index 32dcc8d38e8c..3a9aab9f07d3 100644
--- a/e2e-tests/playwright/mock_file_server.js
+++ b/e2e-tests/playwright/mock_file_server.js
@@ -100,7 +100,9 @@ const server = createServer((req, res) => {
});
});
-server.listen(PORT, '127.0.0.1', () => {
+// Bind to all interfaces, not just loopback, so the Mattermost server container can reach this
+// over the Docker network in `testcontainers` mode.
+server.listen(PORT, () => {
const address = server.address();
const actualPort = typeof address === 'object' && address ? address.port : PORT;
console.log(`File server serving ${ASSET_DIR} on port ${actualPort}!`);
diff --git a/e2e-tests/playwright/package-lock.json b/e2e-tests/playwright/package-lock.json
index aa8ef529c235..00a659266b52 100644
--- a/e2e-tests/playwright/package-lock.json
+++ b/e2e-tests/playwright/package-lock.json
@@ -20,6 +20,7 @@
"@types/luxon": "3.7.2",
"@types/pg": "8.20.0",
"@typescript-eslint/eslint-plugin": "8.62.0",
+ "chalk": "5.6.2",
"cross-env": "10.1.0",
"dayjs": "1.11.21",
"eslint": "9.39.4",
@@ -105,17 +106,22 @@
"license": "MIT",
"dependencies": {
"@axe-core/playwright": "4.12.1",
+ "@azure/storage-blob": "12.33.0",
"@mattermost/client": "file:../../../webapp/platform/client",
"@mattermost/types": "file:../../../webapp/platform/types",
"@percy/cli": "1.32.2",
"@percy/playwright": "1.1.0",
+ "@testcontainers/postgresql": "12.0.4",
"async-wait-until": "2.0.31",
"axe-core": "4.12.1",
"chalk": "5.6.2",
"deepmerge": "4.3.1",
"dotenv": "17.4.2",
+ "ldapts": "9.0.0",
"luxon": "3.7.2",
"mime-types": "3.0.2",
+ "minio": "8.0.7",
+ "testcontainers": "12.0.4",
"uuid": "14.0.1"
},
"devDependencies": {
@@ -130,14 +136,14 @@
"@playwright/test": ">=1.59.0"
}
},
- "lib/node_modules/chalk": {
- "version": "5.6.2",
+ "lib/node_modules/@types/node": {
+ "version": "26.0.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz",
+ "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==",
+ "dev": true,
"license": "MIT",
- "engines": {
- "node": "^12.17.0 || ^14.13 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
+ "dependencies": {
+ "undici-types": "~8.3.0"
}
},
"node_modules/@arcanis/slice-ansi": {
@@ -162,6 +168,208 @@
"playwright-core": ">= 1.0.0"
}
},
+ "node_modules/@azure/abort-controller": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz",
+ "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@azure/core-auth": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz",
+ "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-util": "^1.13.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@azure/core-client": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.0.tgz",
+ "integrity": "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-auth": "^1.10.0",
+ "@azure/core-rest-pipeline": "^1.22.0",
+ "@azure/core-tracing": "^1.3.0",
+ "@azure/core-util": "^1.13.0",
+ "@azure/logger": "^1.3.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@azure/core-http-compat": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.5.0.tgz",
+ "integrity": "sha512-BoSmXPx2er1Ai+wKlDvj29jIQespCNBwEmKyZVHO2kEFsWbGjAjwMCGzug3DJM5/QYIV3vej0S1zcU5bq9fa8w==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ },
+ "peerDependencies": {
+ "@azure/core-client": "^1.10.0",
+ "@azure/core-rest-pipeline": "^1.22.0"
+ }
+ },
+ "node_modules/@azure/core-lro": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz",
+ "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.0.0",
+ "@azure/core-util": "^1.2.0",
+ "@azure/logger": "^1.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-paging": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.7.0.tgz",
+ "integrity": "sha512-7GEAoIsaoBr6KELNRb8nypowCqvk8dnCHFCYg4XD4lOQGY2GqjQg5IhkRjyBFRO18CGSMq05PaNqSOE9GQro3g==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@azure/core-rest-pipeline": {
+ "version": "1.25.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz",
+ "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-auth": "^1.10.0",
+ "@azure/core-tracing": "^1.3.0",
+ "@azure/core-util": "^1.13.0",
+ "@azure/logger": "^1.3.0",
+ "@typespec/ts-http-runtime": "^0.3.4",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@azure/core-tracing": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz",
+ "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@azure/core-util": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz",
+ "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@typespec/ts-http-runtime": "^0.3.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@azure/core-xml": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.6.0.tgz",
+ "integrity": "sha512-e7lX/dk//F6Qf7BB6PTY4+p2yuOQtyOeHGyapYHNwqSp2OnYpwQt49A/Nin2XmKBQ69pwagR4k/lQBq8lbHQkA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-xml-parser": "^5.5.9",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@azure/logger": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz",
+ "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@typespec/ts-http-runtime": "^0.3.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@azure/storage-blob": {
+ "version": "12.33.0",
+ "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.33.0.tgz",
+ "integrity": "sha512-2SX8oP8PyblUcAFZSg39c8Ls+tFjavM6sBeV+qpw33mRzRhI/5hrFJmJ/x0H9xx5l6ECPvgSP8uPxqTeVbHNIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-auth": "^1.9.0",
+ "@azure/core-client": "^1.9.3",
+ "@azure/core-http-compat": "^2.2.0",
+ "@azure/core-lro": "^2.2.0",
+ "@azure/core-paging": "^1.6.2",
+ "@azure/core-rest-pipeline": "^1.19.1",
+ "@azure/core-tracing": "^1.2.0",
+ "@azure/core-util": "^1.11.0",
+ "@azure/core-xml": "^1.4.5",
+ "@azure/logger": "^1.1.4",
+ "@azure/storage-common": "^12.4.1",
+ "events": "^3.0.0",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@azure/storage-common": {
+ "version": "12.4.1",
+ "resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.4.1.tgz",
+ "integrity": "sha512-t14unw/WofGDUi7TKJrsyXyPsN+NLgRm7hMaq0llxNmTIzt7f257+6LE6FKIJPh88zLj6M7LPvzve0fEYg/L3A==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-auth": "^1.9.0",
+ "@azure/core-http-compat": "^2.2.0",
+ "@azure/core-rest-pipeline": "^1.24.0",
+ "@azure/core-tracing": "^1.2.0",
+ "@azure/core-util": "^1.11.0",
+ "@azure/logger": "^1.1.4",
+ "events": "^3.3.0",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -185,6 +393,12 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@balena/dockerignore": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
+ "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
+ "license": "Apache-2.0"
+ },
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -227,9 +441,9 @@
"license": "MIT"
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.9.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
- "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
+ "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -270,17 +484,10 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
- "node_modules/@eslint/config-array/node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
- "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -328,9 +535,9 @@
}
},
"node_modules/@eslint/eslintrc": {
- "version": "3.3.5",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
- "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz",
+ "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -340,7 +547,7 @@
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
- "js-yaml": "^4.1.1",
+ "js-yaml": "^4.3.0",
"minimatch": "^3.1.5",
"strip-json-comments": "^3.1.1"
},
@@ -351,17 +558,27 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/@eslint/eslintrc/node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "node_modules/@eslint/eslintrc/node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
- "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -392,6 +609,13 @@
"node": ">= 4"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -539,6 +763,102 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -562,6 +882,15 @@
"url": "https://opencollective.com/js-sdsl"
}
},
+ "node_modules/@kwsites/file-exists": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
+ "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1"
+ }
+ },
"node_modules/@mattermost/client": {
"resolved": "../../webapp/platform/client",
"link": true
@@ -597,6 +926,18 @@
"@emnapi/runtime": "^1.7.1"
}
},
+ "node_modules/@nodable/entities": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz",
+ "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodable"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -735,36 +1076,6 @@
"node": ">=14"
}
},
- "node_modules/@percy/cli-doctor/node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "license": "MIT"
- },
- "node_modules/@percy/cli-doctor/node_modules/brace-expansion": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
- "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@percy/cli-doctor/node_modules/minimatch": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
- "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.2"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/@percy/cli-exec": {
"version": "1.32.2",
"resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.32.2.tgz",
@@ -838,28 +1149,6 @@
"node": ">=14"
}
},
- "node_modules/@percy/config/node_modules/ajv": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
- "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/@percy/config/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "license": "MIT"
- },
"node_modules/@percy/core": {
"version": "1.32.2",
"resolved": "https://registry.npmjs.org/@percy/core/-/core-1.32.2.tgz",
@@ -896,15 +1185,33 @@
"@percy/cli-doctor": "1.32.2"
}
},
- "node_modules/@percy/core/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
+ "node_modules/@percy/core/node_modules/fast-xml-parser": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.7.tgz",
+ "integrity": "sha512-a6Qh1RMCNbSrU1+sAyAAZH3rTe+OaWJbNZIq0S+ifZciUUOQtlVxBJwoTUE2bYhysmG/RYyI5WJFIKdBahJdrQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "strnum": "^1.0.5"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
+ "node_modules/@percy/core/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/@percy/core/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
@@ -917,6 +1224,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/@percy/core/node_modules/strnum": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
+ "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@percy/dom": {
"version": "1.32.2",
"resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.32.2.tgz",
@@ -996,6 +1315,16 @@
"node": ">=14"
}
},
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@playwright/test": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
@@ -1092,9 +1421,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
- "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
+ "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
"license": "BSD-3-Clause"
},
"node_modules/@rollup/plugin-typescript": {
@@ -1239,6 +1568,9 @@
"arm"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1253,6 +1585,9 @@
"arm"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1267,6 +1602,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1281,6 +1619,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1295,6 +1636,9 @@
"loong64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1309,6 +1653,9 @@
"loong64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1323,6 +1670,9 @@
"ppc64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1337,6 +1687,9 @@
"ppc64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1351,6 +1704,9 @@
"riscv64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1365,6 +1721,9 @@
"riscv64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1379,6 +1738,9 @@
"s390x"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1393,6 +1755,9 @@
"x64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1407,6 +1772,9 @@
"x64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -1511,9 +1879,9 @@
}
},
"node_modules/@snyk/dep-graph": {
- "version": "2.16.9",
- "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-2.16.9.tgz",
- "integrity": "sha512-sXjyY0+r+jwS1Tu5MSg2HyT4E/nEeo5zl9lxcmQIzfFl3jIUum5qxOj24XszJO8X3HgdmOByGQvi62Rkg9fePg==",
+ "version": "2.16.11",
+ "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-2.16.11.tgz",
+ "integrity": "sha512-68HXv7bVBgdZocdyr+bhjK5jtMlnXn1kAIIELRwz2rKNB/ADaml3RaYDsQ3MLlduDh7tIQKIAvQD6FSvv4m3ng==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
@@ -1542,14 +1910,17 @@
}
},
"node_modules/@snyk/error-catalog-nodejs-public": {
- "version": "5.82.0",
- "resolved": "https://registry.npmjs.org/@snyk/error-catalog-nodejs-public/-/error-catalog-nodejs-public-5.82.0.tgz",
- "integrity": "sha512-nBhOvDHTeRMQ3ARYDxtAf7CLEs+c50cT86SaWaADmsdxOcMdzHF13pNjiYxN6hiKhuffyKLPdK+6VF3UQze7hg==",
+ "version": "5.82.1",
+ "resolved": "https://registry.npmjs.org/@snyk/error-catalog-nodejs-public/-/error-catalog-nodejs-public-5.82.1.tgz",
+ "integrity": "sha512-fchNDV+LtJGEJVqtiPyOG3kaUT/LALohZygf32Uzly3UTaxbJvT7wJn5PZiDtP6A8+YJT8klyiYiH3lvdIRCgQ==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"tslib": "^2.8.1",
"uuid": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@snyk/error-catalog-nodejs-public/node_modules/uuid": {
@@ -1603,6 +1974,15 @@
"node": ">=10"
}
},
+ "node_modules/@testcontainers/postgresql": {
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-12.0.4.tgz",
+ "integrity": "sha512-a/pLU6j5lpKKAlUTPwqweqMGhOSjgTSb6HBX69TOrXn32ifU37nnQDmNFTj8ddOAw+BQL9oTRkeOxVbZkqhgZA==",
+ "license": "MIT",
+ "dependencies": {
+ "testcontainers": "^12.0.4"
+ }
+ },
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
@@ -1633,6 +2013,27 @@
"@types/responselike": "^1.0.0"
}
},
+ "node_modules/@types/docker-modem": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz",
+ "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/ssh2": "*"
+ }
+ },
+ "node_modules/@types/dockerode": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-4.0.1.tgz",
+ "integrity": "sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/docker-modem": "*",
+ "@types/node": "*",
+ "@types/ssh2": "*"
+ }
+ },
"node_modules/@types/emscripten": {
"version": "1.41.5",
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
@@ -1714,9 +2115,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "26.0.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz",
- "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==",
+ "version": "26.1.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
+ "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
"license": "MIT",
"dependencies": {
"undici-types": "~8.3.0"
@@ -1751,6 +2152,39 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@types/ssh2": {
+ "version": "1.15.5",
+ "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz",
+ "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "^18.11.18"
+ }
+ },
+ "node_modules/@types/ssh2-streams": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.13.tgz",
+ "integrity": "sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/ssh2/node_modules/@types/node": {
+ "version": "18.19.130",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/@types/ssh2/node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "license": "MIT"
+ },
"node_modules/@types/treeify": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/treeify/-/treeify-1.0.3.tgz",
@@ -1798,17 +2232,17 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz",
- "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==",
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
+ "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "8.62.0",
- "@typescript-eslint/types": "8.62.0",
- "@typescript-eslint/typescript-estree": "8.62.0",
- "@typescript-eslint/visitor-keys": "8.62.0",
+ "@typescript-eslint/scope-manager": "8.65.0",
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1823,15 +2257,68 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
+ "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
+ "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@typescript-eslint/project-service": {
- "version": "8.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz",
- "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==",
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
+ "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.62.0",
- "@typescript-eslint/types": "^8.62.0",
+ "@typescript-eslint/tsconfig-utils": "^8.65.0",
+ "@typescript-eslint/types": "^8.65.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1863,12 +2350,27 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/@typescript-eslint/tsconfig-utils": {
+ "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": {
"version": "8.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz",
- "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz",
+ "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
+ "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -1905,7 +2407,46 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@typescript-eslint/types": {
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": {
+ "version": "8.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz",
+ "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.62.0",
+ "@typescript-eslint/types": "^8.62.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz",
+ "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz",
"integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==",
@@ -1919,7 +2460,7 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/@typescript-eslint/typescript-estree": {
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz",
"integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==",
@@ -1947,43 +2488,335 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@typescript-eslint/utils": {
- "version": "8.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz",
- "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==",
+ "node_modules/@typescript-eslint/type-utils/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.62.0",
- "@typescript-eslint/types": "8.62.0",
- "@typescript-eslint/typescript-estree": "8.62.0"
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
},
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
+ "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz",
- "integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==",
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
+ "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
- "@typescript-eslint/types": "8.62.0",
- "eslint-visitor-keys": "^5.0.0"
+ "@typescript-eslint/project-service": "8.65.0",
+ "@typescript-eslint/tsconfig-utils": "8.65.0",
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
+ "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "peer": true,
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz",
+ "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.62.0",
+ "@typescript-eslint/types": "8.62.0",
+ "@typescript-eslint/typescript-estree": "8.62.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/project-service": {
+ "version": "8.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz",
+ "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.62.0",
+ "@typescript-eslint/types": "^8.62.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz",
+ "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
+ "version": "8.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz",
+ "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz",
+ "integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.62.0",
+ "@typescript-eslint/tsconfig-utils": "8.62.0",
+ "@typescript-eslint/types": "8.62.0",
+ "@typescript-eslint/visitor-keys": "8.62.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz",
+ "integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.62.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": {
+ "version": "8.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz",
+ "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
@@ -2002,6 +2835,20 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@typespec/ts-http-runtime": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.7.tgz",
+ "integrity": "sha512-JVUD8X2tfDMWjcjLs4yVxxVrS8yR5vnh386GAXT9Qj79nBxxXSaHFQZg5FweLmT8HlPQ3kii6noUB+Z9RN7DvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz",
@@ -2108,6 +2955,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2122,6 +2972,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2136,6 +2989,9 @@
"loong64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2150,6 +3006,9 @@
"loong64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2164,6 +3023,9 @@
"ppc64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2178,6 +3040,9 @@
"riscv64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2192,6 +3057,9 @@
"riscv64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2206,6 +3074,9 @@
"s390x"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2220,6 +3091,9 @@
"x64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2234,6 +3108,9 @@
"x64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2353,6 +3230,23 @@
"node": ">=18.12.0"
}
},
+ "node_modules/@yarnpkg/core/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"node_modules/@yarnpkg/core/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -2481,6 +3375,35 @@
"node": ">=18.12.0"
}
},
+ "node_modules/@yarnpkg/shell/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
"node_modules/acorn": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
@@ -2528,16 +3451,15 @@
}
},
"node_modules/ajv": {
- "version": "6.15.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
- "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
- "dev": true,
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
"dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
@@ -2568,6 +3490,129 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/anynum": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz",
+ "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/archiver": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
+ "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "archiver-utils": "^5.0.2",
+ "async": "^3.2.4",
+ "buffer-crc32": "^1.0.0",
+ "readable-stream": "^4.0.0",
+ "readdir-glob": "^1.1.2",
+ "tar-stream": "^3.0.0",
+ "zip-stream": "^6.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/archiver-utils": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz",
+ "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==",
+ "license": "MIT",
+ "dependencies": {
+ "glob": "^10.0.0",
+ "graceful-fs": "^4.2.0",
+ "is-stream": "^2.0.1",
+ "lazystream": "^1.0.0",
+ "lodash": "^4.17.15",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/archiver-utils/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "license": "MIT",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/archiver/node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "license": "MIT",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2584,6 +3629,15 @@
"node": ">=8"
}
},
+ "node_modules/asn1": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
+ "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
@@ -2600,8 +3654,13 @@
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
- "license": "MIT",
- "optional": true
+ "license": "MIT"
+ },
+ "node_modules/async-lock": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
+ "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==",
+ "license": "MIT"
},
"node_modules/async-wait-until": {
"version": "2.0.31",
@@ -2626,16 +3685,126 @@
"node": ">=4"
}
},
+ "node_modules/b4a": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
+ "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "react-native-b4a": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-native-b4a": {
+ "optional": true
+ }
+ }
+ },
"node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true,
- "license": "MIT",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/bare-events": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz",
+ "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "bare-abort-controller": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-fs": {
+ "version": "4.7.4",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz",
+ "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-events": "^2.5.4",
+ "bare-path": "^3.0.0",
+ "bare-stream": "^2.6.4",
+ "bare-url": "^2.2.2",
+ "fast-fifo": "^1.3.2"
+ },
"engines": {
- "node": "18 || 20 || >=22"
+ "bare": ">=1.16.0"
+ },
+ "peerDependencies": {
+ "bare-buffer": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-buffer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz",
+ "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/bare-stream": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz",
+ "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.8.1",
+ "streamx": "^2.25.0",
+ "teex": "^1.0.1"
+ },
+ "peerDependencies": {
+ "bare-abort-controller": "*",
+ "bare-buffer": "*",
+ "bare-events": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ },
+ "bare-buffer": {
+ "optional": true
+ },
+ "bare-events": {
+ "optional": true
+ }
}
},
+ "node_modules/bare-url": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.6.tgz",
+ "integrity": "sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-path": "^3.0.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/basic-ftp": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
@@ -2645,17 +3814,66 @@
"node": ">=10.0.0"
}
},
+ "node_modules/bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bl/node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/block-stream2": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz",
+ "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^3.4.0"
+ }
+ },
"node_modules/brace-expansion": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
- "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
- "dev": true,
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
+ "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"license": "MIT",
"dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
+ "balanced-match": "^1.0.0"
}
},
"node_modules/braces": {
@@ -2670,13 +3888,52 @@
"node": ">=8"
}
},
+ "node_modules/browser-or-node": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz",
+ "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==",
+ "license": "MIT"
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
"node_modules/buffer-crc32": {
- "version": "0.2.13",
- "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
- "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
+ "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
"license": "MIT",
"engines": {
- "node": "*"
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/buildcheck": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz",
+ "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
+ "optional": true,
+ "engines": {
+ "node": ">=10.0.0"
}
},
"node_modules/busboy": {
@@ -2690,6 +3947,15 @@
"node": ">=10.16.0"
}
},
+ "node_modules/byline": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
+ "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
@@ -2739,17 +4005,12 @@
}
},
"node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "devOptional": true,
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
"engines": {
- "node": ">=10"
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
@@ -2859,6 +4120,38 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/compress-commons": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
+ "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==",
+ "license": "MIT",
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "crc32-stream": "^6.0.0",
+ "is-stream": "^2.0.1",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/compress-commons/node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "license": "MIT",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2877,6 +4170,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
"node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
@@ -2903,6 +4202,61 @@
}
}
},
+ "node_modules/cpu-features": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
+ "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "buildcheck": "~0.0.6",
+ "nan": "^2.19.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/crc32-stream": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz",
+ "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==",
+ "license": "MIT",
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/crc32-stream/node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "license": "MIT",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
"node_modules/cross-env": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
@@ -2968,6 +4322,15 @@
}
}
},
+ "node_modules/decode-uri-component": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+ "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -3079,6 +4442,102 @@
"node": ">=8"
}
},
+ "node_modules/docker-compose": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.4.2.tgz",
+ "integrity": "sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==",
+ "license": "MIT",
+ "dependencies": {
+ "yaml": "^2.2.2"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/docker-modem": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz",
+ "integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "readable-stream": "^3.5.0",
+ "split-ca": "^1.0.1",
+ "ssh2": "^1.15.0"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/dockerode": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-5.0.1.tgz",
+ "integrity": "sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@balena/dockerignore": "^1.0.2",
+ "@grpc/grpc-js": "^1.11.1",
+ "@grpc/proto-loader": "^0.7.13",
+ "docker-modem": "^5.0.7",
+ "protobufjs": "^7.3.2",
+ "tar-fs": "^2.1.4"
+ },
+ "engines": {
+ "node": ">= 14.17"
+ }
+ },
+ "node_modules/dockerode/node_modules/@grpc/proto-loader": {
+ "version": "0.7.15",
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
+ "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash.camelcase": "^4.3.0",
+ "long": "^5.0.0",
+ "protobufjs": "^7.2.5",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/dockerode/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/dockerode/node_modules/tar-fs": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
+ "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/dockerode/node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
@@ -3091,6 +4550,12 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -3139,14 +4604,15 @@
}
},
"node_modules/es-toolkit": {
- "version": "1.49.0",
- "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz",
- "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==",
+ "version": "1.50.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz",
+ "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==",
"license": "MIT",
"optional": true,
"workspaces": [
"docs",
- "benchmarks"
+ "benchmarks",
+ "tests/types"
]
},
"node_modules/escalade": {
@@ -3342,17 +4808,27 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "node_modules/eslint/node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
},
"node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
- "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3360,6 +4836,23 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
@@ -3383,6 +4876,13 @@
"node": ">= 4"
}
},
+ "node_modules/eslint/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -3488,17 +4988,53 @@
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
"engines": {
- "node": ">=0.10.0"
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/event-loop-spinner": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/event-loop-spinner/-/event-loop-spinner-2.3.3.tgz",
+ "integrity": "sha512-1mFCR39pkNh0agtlKPXVOBM5Bq2qOC5Pz+wlqizcKPKq2XTreVGDgWBi+Iyb4mdA5nF+oLd6oqePI9AZ2K7xMw==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
}
},
- "node_modules/event-loop-spinner": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/event-loop-spinner/-/event-loop-spinner-2.3.2.tgz",
- "integrity": "sha512-O078Lkxi/yZEPPifcizDOGUeK1OFOlPC6sfCCrx10odvqX3tEi9XLaIRt9cIl9TBFcPZzuMaXbJ0b+T6D2Tnjg==",
+ "node_modules/events-universal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
+ "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
- "optional": true,
"dependencies": {
- "tslib": "^2.6.3"
+ "bare-events": "^2.7.0"
}
},
"node_modules/extract-zip": {
@@ -3527,6 +5063,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "license": "MIT"
+ },
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -3570,9 +5112,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
- "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
+ "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"funding": [
{
"type": "github",
@@ -3585,10 +5127,26 @@
],
"license": "BSD-3-Clause"
},
+ "node_modules/fast-xml-builder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz",
+ "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "path-expression-matcher": "^1.6.2",
+ "xml-naming": "^0.3.0"
+ }
+ },
"node_modules/fast-xml-parser": {
- "version": "4.5.6",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz",
- "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==",
+ "version": "5.10.1",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz",
+ "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==",
"funding": [
{
"type": "github",
@@ -3597,7 +5155,12 @@
],
"license": "MIT",
"dependencies": {
- "strnum": "^1.0.5"
+ "@nodable/entities": "^3.0.0",
+ "fast-xml-builder": "^1.2.0",
+ "is-unsafe": "^2.0.0",
+ "path-expression-matcher": "^1.6.2",
+ "strnum": "^2.4.1",
+ "xml-naming": "^0.3.0"
},
"bin": {
"fxparser": "src/cli/cli.js"
@@ -3664,6 +5227,15 @@
"node": ">=8"
}
},
+ "node_modules/filter-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
+ "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -3696,12 +5268,46 @@
}
},
"node_modules/flatted": {
- "version": "3.4.2",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
- "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz",
+ "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==",
"dev": true,
"license": "ISC"
},
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -3756,6 +5362,18 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-port": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
+ "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -3835,6 +5453,45 @@
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"license": "BSD-2-Clause"
},
+ "node_modules/glob/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/globals": {
"version": "17.7.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
@@ -3868,17 +5525,10 @@
"node": ">=8"
}
},
- "node_modules/globby/node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/globby/node_modules/brace-expansion": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
- "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3961,7 +5611,6 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
"license": "ISC"
},
"node_modules/grapheme-splitter": {
@@ -4051,10 +5700,30 @@
"node": ">= 14"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
- "version": "7.0.5",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
- "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
+ "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4130,14 +5799,23 @@
"license": "ISC"
},
"node_modules/ip-address": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
- "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz",
+ "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
+ "node_modules/ipaddr.js": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz",
+ "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -4219,12 +5897,57 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-unsafe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz",
+ "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4267,10 +5990,9 @@
"license": "MIT"
},
"node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
@@ -4300,6 +6022,60 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/lazystream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.6.3"
+ }
+ },
+ "node_modules/lazystream/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/lazystream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/lazystream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/ldapts": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/ldapts/-/ldapts-9.0.0.tgz",
+ "integrity": "sha512-OaaoYBSuan7g0Nm2e1wsRl+9xol41zY+pDlRRSsBq36iKCd2tG/K8WifevNRDjyEfhz4bvkxKdDvJ6xHXwj7+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "strict-event-emitter-types": "2.0.0"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -4336,6 +6112,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT"
+ },
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -4501,9 +6283,9 @@
}
},
"node_modules/lru-cache": {
- "version": "11.5.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
- "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
@@ -4629,16 +6411,15 @@
}
},
"node_modules/minimatch": {
- "version": "10.2.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
- "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
- "dev": true,
- "license": "BlueOak-1.0.0",
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "license": "ISC",
"dependencies": {
- "brace-expansion": "^5.0.5"
+ "brace-expansion": "^2.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -4654,11 +6435,55 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/minio": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.7.tgz",
+ "integrity": "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.4",
+ "block-stream2": "^2.1.0",
+ "browser-or-node": "^2.1.1",
+ "buffer-crc32": "^1.0.0",
+ "eventemitter3": "^5.0.1",
+ "fast-xml-parser": "^5.3.4",
+ "ipaddr.js": "^2.0.1",
+ "lodash": "^4.17.21",
+ "mime-types": "^2.1.35",
+ "query-string": "^7.1.3",
+ "stream-json": "^1.8.0",
+ "through2": "^4.0.2",
+ "xml2js": "^0.5.0 || ^0.6.2"
+ },
+ "engines": {
+ "node": "^16 || ^18 || >=20"
+ }
+ },
+ "node_modules/minio/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minio/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minipass": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
- "devOptional": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -4677,12 +6502,40 @@
"node": ">= 18"
}
},
+ "node_modules/mkdirp": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+ "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "dist/cjs/src/bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/nan": {
+ "version": "2.28.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz",
+ "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/napi-postinstall": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
@@ -4715,6 +6568,15 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
@@ -4875,6 +6737,12 @@
"node": ">= 14"
}
},
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
"node_modules/packageurl-js": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-2.0.1.tgz",
@@ -4935,7 +6803,22 @@
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-expression-matcher": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz",
+ "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
}
},
"node_modules/path-is-absolute": {
@@ -5105,9 +6988,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5216,10 +7099,53 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
+ "node_modules/proper-lockfile": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
+ "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "retry": "^0.12.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "node_modules/properties-reader": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-3.0.1.tgz",
+ "integrity": "sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==",
+ "license": "MIT",
+ "dependencies": {
+ "@kwsites/file-exists": "^1.1.1",
+ "mkdirp": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/steveukx/properties?sponsor=1"
+ }
+ },
"node_modules/protobufjs": {
- "version": "7.6.4",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz",
- "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==",
+ "version": "7.6.5",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz",
+ "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -5259,6 +7185,24 @@
"node": ">=6"
}
},
+ "node_modules/query-string": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
+ "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
+ "license": "MIT",
+ "dependencies": {
+ "decode-uri-component": "^0.2.2",
+ "filter-obj": "^1.1.0",
+ "split-on-first": "^1.0.0",
+ "strict-uri-encode": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
@@ -5301,6 +7245,41 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdir-glob": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
+ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.1.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/minimatch": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -5380,6 +7359,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -5413,16 +7401,10 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/rimraf/node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "license": "MIT"
- },
"node_modules/rimraf/node_modules/brace-expansion": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
- "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -5567,6 +7549,21 @@
],
"license": "MIT"
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/sax": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
+ "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=11.0.0"
+ }
+ },
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
@@ -5601,6 +7598,12 @@
"node": ">=8"
}
},
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -5723,6 +7726,21 @@
"node": ">=0.10.0"
}
},
+ "node_modules/split-ca": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
+ "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
+ "license": "ISC"
+ },
+ "node_modules/split-on-first": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
+ "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -5740,6 +7758,43 @@
"license": "BSD-3-Clause",
"optional": true
},
+ "node_modules/ssh-remote-port-forward": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz",
+ "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ssh2": "^0.5.48",
+ "ssh2": "^1.4.0"
+ }
+ },
+ "node_modules/ssh-remote-port-forward/node_modules/@types/ssh2": {
+ "version": "0.5.52",
+ "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz",
+ "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/ssh2-streams": "*"
+ }
+ },
+ "node_modules/ssh2": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
+ "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "asn1": "^0.2.6",
+ "bcrypt-pbkdf": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ },
+ "optionalDependencies": {
+ "cpu-features": "~0.0.10",
+ "nan": "^2.23.0"
+ }
+ },
"node_modules/stable-hash-x": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz",
@@ -5773,6 +7828,41 @@
"node": ">=10.0.0"
}
},
+ "node_modules/streamx": {
+ "version": "2.28.0",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz",
+ "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==",
+ "license": "MIT",
+ "dependencies": {
+ "events-universal": "^1.0.0",
+ "fast-fifo": "^1.3.2",
+ "text-decoder": "^1.1.0"
+ }
+ },
+ "node_modules/strict-event-emitter-types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz",
+ "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==",
+ "license": "ISC"
+ },
+ "node_modules/strict-uri-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
+ "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -5787,6 +7877,21 @@
"node": ">=8"
}
},
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -5799,6 +7904,19 @@
"node": ">=8"
}
},
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -5813,16 +7931,19 @@
}
},
"node_modules/strnum": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
- "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz",
+ "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
- "license": "MIT"
+ "license": "MIT",
+ "dependencies": {
+ "anynum": "^1.0.1"
+ }
},
"node_modules/supports-color": {
"version": "7.2.0",
@@ -5851,9 +7972,9 @@
}
},
"node_modules/systeminformation": {
- "version": "5.31.11",
- "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.11.tgz",
- "integrity": "sha512-I6O7iaUj23AXRgCPDDnvi3xHvdOLp4+1YMbF+X194lJwY1NeWojgHJPhslVKcmTtrLTguRk3QJK+xEdTiI3P0w==",
+ "version": "5.33.1",
+ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.1.tgz",
+ "integrity": "sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w==",
"license": "MIT",
"os": [
"darwin",
@@ -5869,7 +7990,7 @@
"systeminformation": "lib/cli.js"
},
"engines": {
- "node": ">=8.0.0"
+ "node": ">=10.0.0"
},
"funding": {
"type": "Buy me a coffee",
@@ -5877,9 +7998,9 @@
}
},
"node_modules/tar": {
- "version": "7.5.19",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz",
- "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==",
+ "version": "7.5.22",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
+ "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
"license": "BlueOak-1.0.0",
"optional": true,
"dependencies": {
@@ -5893,6 +8014,82 @@
"node": ">=18"
}
},
+ "node_modules/tar-fs": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz",
+ "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ },
+ "optionalDependencies": {
+ "bare-fs": "^4.0.1",
+ "bare-path": "^3.0.0"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
+ "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
+ "license": "MIT",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "bare-fs": "^4.5.5",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/teex": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
+ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
+ "license": "MIT",
+ "dependencies": {
+ "streamx": "^2.12.5"
+ }
+ },
+ "node_modules/testcontainers": {
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-12.0.4.tgz",
+ "integrity": "sha512-QIR/8xF1+F/26cIM+9B4yyxNTbKJxAv3hygZyhPRgZ8Q2AhlPZjDdpXRuk16V37X4bgJRI3hXFhoEICMBA7Adg==",
+ "license": "MIT",
+ "dependencies": {
+ "@balena/dockerignore": "^1.0.2",
+ "@types/dockerode": "^4.0.1",
+ "archiver": "^7.0.1",
+ "async-lock": "^1.4.1",
+ "byline": "^5.0.0",
+ "debug": "^4.4.3",
+ "docker-compose": "^1.4.2",
+ "dockerode": "^5.0.0",
+ "get-port": "^5.1.1",
+ "proper-lockfile": "^4.1.2",
+ "properties-reader": "^3.0.1",
+ "ssh-remote-port-forward": "^1.0.4",
+ "tar-fs": "^3.1.2",
+ "tmp": "^0.2.7",
+ "undici": "^8.5.0"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
+ "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.6.4"
+ }
+ },
+ "node_modules/through2": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
+ "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "3"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -5917,6 +8114,15 @@
"license": "MIT",
"optional": true
},
+ "node_modules/tmp": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
+ "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -5958,6 +8164,12 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
+ "license": "Unlicense"
+ },
"node_modules/typanion": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/typanion/-/typanion-3.14.0.tgz",
@@ -5995,6 +8207,15 @@
"node": ">=14.17"
}
},
+ "node_modules/undici": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",
+ "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.19.0"
+ }
+ },
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
@@ -6059,6 +8280,12 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
"node_modules/uuid": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
@@ -6114,6 +8341,24 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -6121,9 +8366,9 @@
"license": "ISC"
},
"node_modules/ws": {
- "version": "8.21.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
- "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "version": "8.21.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
+ "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -6141,6 +8386,43 @@
}
}
},
+ "node_modules/xml-naming": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz",
+ "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -6222,6 +8504,15 @@
"fd-slicer": "~1.1.0"
}
},
+ "node_modules/yauzl/node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -6235,6 +8526,36 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zip-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
+ "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==",
+ "license": "MIT",
+ "dependencies": {
+ "archiver-utils": "^5.0.0",
+ "compress-commons": "^6.0.2",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/zip-stream/node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "license": "MIT",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
diff --git a/e2e-tests/playwright/package.json b/e2e-tests/playwright/package.json
index 5ef5464bae63..76d54a2d2cba 100644
--- a/e2e-tests/playwright/package.json
+++ b/e2e-tests/playwright/package.json
@@ -21,6 +21,9 @@
"test:visual-update-snapshots": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test specs/visual --grep @visual --update-snapshots",
"test:update-snapshots": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test --project=chrome --grep @snapshots --update-snapshots --update-source-method=overwrite",
"test:slomo": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true PW_SLOWMO=1000 playwright test",
+ "test:full": "npm run build && cross-env PW_USE_TESTCONTAINERS=true PW_SNAPSHOT_ENABLE=true playwright test",
+ "testcontainers:up": "npm run build && cross-env PW_USE_TESTCONTAINERS=true playwright test --config=playwright.testcontainers-up.config.ts",
+ "testcontainers:down": "node script/testcontainers_down.mjs",
"percy:docker": "npm run build && cross-env PW_PERCY_ENABLE=true PERCY_BROWSER_EXECUTABLE='/ms-playwright/chromium-1169/chrome-linux/chrome' percy exec -- playwright test specs/visual --grep @visual --project=chrome --project=ipad",
"codegen": "npm run build && cross-env playwright codegen $PW_BASE_URL",
"playwright-ui": "npm run build && cross-env playwright test --ui",
@@ -39,16 +42,25 @@
"@types/luxon": "3.7.2",
"@types/pg": "8.20.0",
"@typescript-eslint/eslint-plugin": "8.62.0",
+ "chalk": "5.6.2",
"cross-env": "10.1.0",
"dayjs": "1.11.21",
"eslint": "9.39.4",
"eslint-import-resolver-typescript": "4.4.5",
- "globals": "17.7.0",
"glob": "13.0.6",
+ "globals": "17.7.0",
"luxon": "3.7.2",
"pg": "8.22.0",
"prettier": "3.9.1",
"typescript": "6.0.3",
"zod": "4.4.3"
+ },
+ "allowScripts": {
+ "cpu-features@0.0.10": true,
+ "protobufjs@7.6.4": true,
+ "ssh2@1.17.0": true,
+ "unrs-resolver@1.12.2": true,
+ "@percy/core@1.32.2": true,
+ "fsevents@2.3.2": true
}
}
diff --git a/e2e-tests/playwright/playwright.testcontainers-up.config.ts b/e2e-tests/playwright/playwright.testcontainers-up.config.ts
new file mode 100644
index 000000000000..2bcbe392c7c8
--- /dev/null
+++ b/e2e-tests/playwright/playwright.testcontainers-up.config.ts
@@ -0,0 +1,15 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {defineConfig} from '@playwright/test';
+
+// Standalone config for `npm run testcontainers:up`: brings up (or reuses) the Testcontainers
+// stack via its own global setup — deliberately not the real suite's global_setup.ts, so the
+// server comes up fresh and untouched by baseGlobalSetup()'s admin/sysadmin bootstrap. Runs a
+// single no-op test so Playwright has something to execute global setup/teardown around, without
+// pulling in the main playwright.config.ts's projects/browsers or touching its `specs` testDir.
+export default defineConfig({
+ globalSetup: './script/testcontainers_up_global_setup.ts',
+ testDir: './script',
+ testMatch: /testcontainers_up\.spec\.ts/,
+});
diff --git a/e2e-tests/playwright/script/testcontainers_down.mjs b/e2e-tests/playwright/script/testcontainers_down.mjs
new file mode 100644
index 000000000000..a7fe95f036e6
--- /dev/null
+++ b/e2e-tests/playwright/script/testcontainers_down.mjs
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+// Reused containers are deliberately left running after a Playwright process exits, so there's no
+// in-process teardown to call from a fresh invocation of this script — it finds and removes them
+// directly via Docker, by the label every container this project starts carries.
+//
+// Also doubles as CI's final teardown step: with PW_TESTCONTAINERS_REUSE, the stack stays up
+// across every per-spec test invocation in a job, so none of them ever run the real (non-adopted)
+// teardown path that collects container logs or archives the boot-env drift history. This script
+// does both before removing anything, so `ci/upload-debug-artifacts` still picks them up.
+
+import {execSync} from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+
+const LABEL_FILTER = 'label=mm-playwright-testcontainers=true';
+const LOG_DIR = path.resolve(process.cwd(), 'logs');
+const ENV_FILE_PATH = path.resolve(process.cwd(), '.env.testcontainers');
+
+function run(command) {
+ try {
+ return execSync(command, {encoding: 'utf-8'}).trim();
+ } catch {
+ return '';
+ }
+}
+
+// Reads the last-written PW_TESTCONTAINERS_NETWORK_NAME= line from .env.testcontainers before
+// it's archived/removed, so this run's network can be removed by name instead of via a host-wide
+// `docker network prune`. Entries are appended on each write, so the last match is current.
+function readTestcontainersNetworkName() {
+ if (!fs.existsSync(ENV_FILE_PATH)) {
+ return undefined;
+ }
+ const matches = [...fs.readFileSync(ENV_FILE_PATH, 'utf-8').matchAll(/^PW_TESTCONTAINERS_NETWORK_NAME=(.*)$/gm)];
+ return matches.at(-1)?.[1]?.trim() || undefined;
+}
+
+// Archives, then removes, .env.testcontainers — a stale copy left behind would seed the next
+// fresh boot's config overrides from a container that no longer exists, wrongly convincing a
+// later pw.ensure*() call that some old setting is already active.
+function archiveEnvFile() {
+ if (!fs.existsSync(ENV_FILE_PATH)) {
+ return;
+ }
+ fs.mkdirSync(LOG_DIR, {recursive: true});
+ fs.copyFileSync(ENV_FILE_PATH, path.join(LOG_DIR, 'testcontainers_env_history.log'));
+ fs.rmSync(ENV_FILE_PATH);
+}
+
+function collectLogs(containerIds) {
+ fs.mkdirSync(LOG_DIR, {recursive: true});
+ for (const id of containerIds) {
+ const image = run(`docker inspect -f "{{.Config.Image}}" ${id}`) || id;
+ const safeName = image.replace(/[^a-zA-Z0-9_.-]/g, '_');
+ const outPath = path.join(LOG_DIR, `${safeName}-${id.slice(0, 12)}.log`);
+ try {
+ execSync(`docker logs "${id}" > "${outPath}" 2>&1`);
+ } catch {
+ // Best-effort — a container that's already gone or never logged anything shouldn't
+ // block teardown.
+ }
+ }
+}
+
+const containerIds = run(`docker ps -aq --filter "${LABEL_FILTER}"`).split('\n').filter(Boolean);
+
+const networkName = readTestcontainersNetworkName();
+archiveEnvFile();
+
+if (containerIds.length === 0) {
+ // eslint-disable-next-line no-console
+ console.log('No Testcontainers-managed containers found (nothing to remove).');
+} else {
+ collectLogs(containerIds);
+
+ // eslint-disable-next-line no-console
+ console.log(`Removing ${containerIds.length} Testcontainers-managed container(s): ${containerIds.join(', ')}`);
+ execSync(`docker rm -f ${containerIds.join(' ')}`, {stdio: 'inherit'});
+
+ if (networkName) {
+ // eslint-disable-next-line no-console
+ console.log(`Removing Testcontainers-managed network: ${networkName}`);
+ run(`docker network rm ${networkName}`);
+ }
+}
diff --git a/e2e-tests/playwright/script/testcontainers_up.spec.ts b/e2e-tests/playwright/script/testcontainers_up.spec.ts
new file mode 100644
index 000000000000..8b19b9263334
--- /dev/null
+++ b/e2e-tests/playwright/script/testcontainers_up.spec.ts
@@ -0,0 +1,9 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@playwright/test';
+
+// No-op: playwright.testcontainers-up.config.ts's globalSetup (the same global_setup.ts the real
+// suite uses) already brought the stack up, or reused one already running. This test exists only
+// because Playwright's global setup/teardown fire around an actual test run, not standalone.
+test('testcontainers stack is up', () => {});
diff --git a/e2e-tests/playwright/script/testcontainers_up_global_setup.ts b/e2e-tests/playwright/script/testcontainers_up_global_setup.ts
new file mode 100644
index 000000000000..99945dbbc6e4
--- /dev/null
+++ b/e2e-tests/playwright/script/testcontainers_up_global_setup.ts
@@ -0,0 +1,27 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import chalk from 'chalk';
+
+import {startStack, stopStack} from '@mattermost/playwright-lib';
+
+// Deliberately skips baseGlobalSetup() (admin user bootstrap, sysadmin setup, plugin config):
+// `npm run testcontainers:up` is for bringing up a fresh, untouched server for poking around or
+// for a later `npm run test` to reuse — not for preparing it as if a real suite were about to run.
+async function globalSetup() {
+ try {
+ await startStack();
+ } catch (error: unknown) {
+ // eslint-disable-next-line no-console
+ console.error(chalk.cyan('[testcontainers]'), error);
+ await stopStack();
+ const message = error instanceof Error ? error.message : String(error);
+ throw new Error(chalk.red(`[testcontainers] stack failed to start: ${message}`));
+ }
+
+ return async function () {
+ await stopStack();
+ };
+}
+
+export default globalSetup;
diff --git a/e2e-tests/playwright/specs/functional/channels/interactive_messages/mm_blocks_ephemeral.spec.ts b/e2e-tests/playwright/specs/functional/channels/interactive_messages/mm_blocks_ephemeral.spec.ts
index 3912ef2ccad7..9fdc6a91c7f1 100644
--- a/e2e-tests/playwright/specs/functional/channels/interactive_messages/mm_blocks_ephemeral.spec.ts
+++ b/e2e-tests/playwright/specs/functional/channels/interactive_messages/mm_blocks_ephemeral.spec.ts
@@ -63,7 +63,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -89,7 +89,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
});
const ephemeralMarker = `E2E mm_blocks ephemeral action ${pw.random.id()}`;
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration`;
await adminClient.createPostEphemeral(user.id, {
channel_id: townSquare.id,
@@ -153,7 +153,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -177,7 +177,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
});
const ephemeralMarker = `E2E mm_blocks ephemeral update ${pw.random.id()}`;
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_update`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_update`;
await adminClient.createPostEphemeral(user.id, {
channel_id: townSquare.id,
@@ -238,7 +238,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -264,7 +264,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
const overrideAuthorName = 'Playwright mm_blocks eph override';
const ephemeralMarker = `E2E mm_blocks ephemeral override ${pw.random.id()}`;
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_update`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_update`;
await adminClient.createPostEphemeral(user.id, {
channel_id: townSquare.id,
@@ -331,7 +331,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -355,7 +355,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
});
const ephemeralMarker = `E2E mm_blocks eph query merge ${pw.random.id()}`;
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_echo_query`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_echo_query`;
await adminClient.createPostEphemeral(user.id, {
channel_id: townSquare.id,
@@ -418,7 +418,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -442,7 +442,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
});
const ephemeralMarker = `E2E mm_blocks eph query override ${pw.random.id()}`;
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_echo_query`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_echo_query`;
await adminClient.createPostEphemeral(user.id, {
channel_id: townSquare.id,
@@ -501,7 +501,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -525,7 +525,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
});
const ephemeralMarker = `E2E mm_blocks eph static_select query ${pw.random.id()}`;
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_echo_query`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_echo_query`;
await adminClient.createPostEphemeral(user.id, {
channel_id: townSquare.id,
@@ -596,7 +596,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -620,7 +620,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
});
const ephemeralMarker = `E2E mm_blocks eph static_select users ${pw.random.id()}`;
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_static_select`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_static_select`;
await adminClient.createPostEphemeral(user.id, {
channel_id: townSquare.id,
@@ -690,7 +690,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -714,7 +714,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
});
const ephemeralMarker = `E2E mm_blocks eph static_select channels ${pw.random.id()}`;
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_static_select`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_static_select`;
await adminClient.createPostEphemeral(user.id, {
channel_id: townSquare.id,
@@ -783,7 +783,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -808,7 +808,7 @@ test.describe('Interactive mm_blocks (ephemeral post)', () => {
const contextMarker = `ctx_${pw.random.id()}`;
const ephemeralMarker = `E2E mm_blocks eph action_context ${pw.random.id()}`;
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_echo_context`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_echo_context`;
await adminClient.createPostEphemeral(user.id, {
channel_id: townSquare.id,
diff --git a/e2e-tests/playwright/specs/functional/channels/interactive_messages/mm_blocks_incoming_webhook.spec.ts b/e2e-tests/playwright/specs/functional/channels/interactive_messages/mm_blocks_incoming_webhook.spec.ts
index 82ee9df081c0..1028d609ecb9 100644
--- a/e2e-tests/playwright/specs/functional/channels/interactive_messages/mm_blocks_incoming_webhook.spec.ts
+++ b/e2e-tests/playwright/specs/functional/channels/interactive_messages/mm_blocks_incoming_webhook.spec.ts
@@ -237,7 +237,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -255,7 +255,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
display_name: 'Playwright mm_blocks integration',
});
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration`;
await postIncomingWebhook(webhook.id, {
text: 'E2E mm_blocks external integration',
@@ -317,7 +317,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -335,7 +335,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
display_name: 'Playwright mm_blocks update',
});
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_update`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_update`;
await postIncomingWebhook(webhook.id, {
text: 'E2E mm_blocks before apply update',
@@ -385,7 +385,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -406,7 +406,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
const overrideAuthorName = 'Playwright mm_blocks override';
const marker = `E2E mm_blocks override author ${pw.random.id()}`;
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_update`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_update`;
await postIncomingWebhook(webhook.id, {
username: overrideAuthorName,
@@ -466,7 +466,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -484,7 +484,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
display_name: 'Playwright mm_blocks static_select',
});
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_static_select`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_static_select`;
const marker = `E2E mm_blocks static_select ${pw.random.id()}`;
await postIncomingWebhook(webhook.id, {
@@ -561,7 +561,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -579,7 +579,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
display_name: 'Playwright mm_blocks static_select users',
});
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_static_select`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_static_select`;
const marker = `E2E mm_blocks static_select users ${pw.random.id()}`;
await postIncomingWebhook(webhook.id, {
@@ -655,7 +655,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -673,7 +673,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
display_name: 'Playwright mm_blocks static_select channels',
});
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_static_select`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_static_select`;
const marker = `E2E mm_blocks static_select channels ${pw.random.id()}`;
await postIncomingWebhook(webhook.id, {
@@ -748,7 +748,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -767,7 +767,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
});
const contextMarker = `ctx_${pw.random.id()}`;
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_echo_context`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_echo_context`;
const marker = `E2E mm_blocks action_context ${pw.random.id()}`;
await postIncomingWebhook(webhook.id, {
@@ -888,7 +888,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -906,7 +906,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
display_name: 'Playwright mm_blocks button query',
});
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_echo_query`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_echo_query`;
const marker = `E2E mm_blocks button query ${pw.random.id()}`;
await postIncomingWebhook(webhook.id, {
@@ -974,7 +974,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -992,7 +992,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
display_name: 'Playwright mm_blocks query override',
});
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_echo_query`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_echo_query`;
const marker = `E2E mm_blocks query override ${pw.random.id()}`;
await postIncomingWebhook(webhook.id, {
@@ -1049,7 +1049,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
);
await setupWebhookTestServer(request, {
- mattermostBaseUrl: testConfig.baseURL,
+ mattermostBaseUrl: testConfig.internalBaseURL,
adminUsername: testConfig.adminUsername,
adminPassword: testConfig.adminPassword,
});
@@ -1067,7 +1067,7 @@ test.describe('Interactive mm_blocks (incoming webhook)', () => {
display_name: 'Playwright mm_blocks select query',
});
- const integrationUrl = `${testConfig.webhookBaseUrl}/mm_blocks_integration_echo_query`;
+ const integrationUrl = `${testConfig.webhookInternalUrl}/mm_blocks_integration_echo_query`;
const marker = `E2E mm_blocks static_select query ${pw.random.id()}`;
await postIncomingWebhook(webhook.id, {
diff --git a/e2e-tests/playwright/specs/functional/channels/post_list/post_height.spec.ts b/e2e-tests/playwright/specs/functional/channels/post_list/post_height.spec.ts
index 9626eb55c346..32f0d9c71d6c 100644
--- a/e2e-tests/playwright/specs/functional/channels/post_list/post_height.spec.ts
+++ b/e2e-tests/playwright/specs/functional/channels/post_list/post_height.spec.ts
@@ -36,12 +36,15 @@ test.describe('Post height', () => {
type: 'O',
});
- // # Enable SVG rendering and let the server fetch metadata from the local mock file server
+ // # Enable SVG rendering and let the server fetch metadata from the mock file server.
+ // AllowedUntrustedInternalConnections only takes effect in `external` mode here — in
+ // `testcontainers` mode it's fixed at boot via an env var, and a PatchConfig on an env-controlled
+ // field is accepted but has no real effect.
await adminClient.patchConfig({
ServiceSettings: {
EnableSVGs: true,
EnableLinkPreviews: true,
- AllowedUntrustedInternalConnections: 'localhost 127.0.0.1',
+ AllowedUntrustedInternalConnections: `localhost 127.0.0.1 ${new URL(fileServerUrl).hostname}`,
},
});
@@ -277,8 +280,11 @@ test.describe('Post height', () => {
},
{
name: 'post with an SVG Markdown image',
- // TODO Either Chrome preloads the SVG's dimensions early or Firefox doesn't allocate the height properly
- skipProjects: ['firefox'],
+ // Markdown/remote SVGs intentionally receive no server-provided dimensions
+ // (SVG images are filtered from link metadata to mitigate the MM-67372 DoS),
+ // so the client cannot reserve height before the SVG loads. That makes a
+ // layout-shift-free render impossible for this case regardless of browser.
+ skipProjects: ['chrome', 'firefox', 'ipad'],
getSeedOptions: (baseUrl) => ({
message: ``,
}),
diff --git a/e2e-tests/playwright/specs/functional/system_console/abac/file_access/file_permissions_download.spec.ts b/e2e-tests/playwright/specs/functional/system_console/abac/file_access/file_permissions_download.spec.ts
index 5e51a6d4ba8a..61f0bb68f1fb 100644
--- a/e2e-tests/playwright/specs/functional/system_console/abac/file_access/file_permissions_download.spec.ts
+++ b/e2e-tests/playwright/specs/functional/system_console/abac/file_access/file_permissions_download.spec.ts
@@ -1,7 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {expect, test, enableABAC, getAdminClient, TestBrowser, getRandomId} from '@mattermost/playwright-lib';
+import {
+ expect,
+ test,
+ enableABAC,
+ getAdminClient,
+ TestBrowser,
+ getRandomId,
+ testConfig,
+} from '@mattermost/playwright-lib';
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
@@ -350,8 +358,10 @@ test.describe('ABAC Permission Policies - BOR and Permalink', () => {
// # Retrieve the post ID and construct the permalink URL
const postsResult = await adminClient.getPosts(channelId, 0, 1);
const postId = postsResult.order[0];
- const serverUrl = adminClient.getBaseRoute().replace('/api/v4', '');
- const permalinkUrl = `${serverUrl}/${team.name}/pl/${postId}`;
+ // Use testConfig.internalBaseURL, not adminClient's host-mapped route — the server itself
+ // must recognize this URL as its own SiteURL to embed it via an internal permalink lookup,
+ // instead of trying (and, in `testcontainers` mode, failing) to fetch it back over HTTP as a link.
+ const permalinkUrl = `${testConfig.internalBaseURL}/${team.name}/pl/${postId}`;
// # Admin posts the permalink in the same channel (creates an embedded preview)
await adminChannelsPage.centerView.postCreate.postMessage(permalinkUrl);
diff --git a/e2e-tests/playwright/specs/functional/system_console/abac/masking/masking_db_setup.ts b/e2e-tests/playwright/specs/functional/system_console/abac/masking/masking_db_setup.ts
index 0705ba60dec7..d1e1af0fcbfc 100644
--- a/e2e-tests/playwright/specs/functional/system_console/abac/masking/masking_db_setup.ts
+++ b/e2e-tests/playwright/specs/functional/system_console/abac/masking/masking_db_setup.ts
@@ -14,22 +14,17 @@
* Helpers use a one-shot `pg.Client` so we don't keep a connection pool open
* for the lifetime of the test run, and use parameterized queries throughout.
*
- * DB URL resolution order:
- * 1. MM_TEST_DB_URL env var
- * 2. default: postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable
+ * Connects via testConfig.postgresUrl, which resolves to the fixed localhost:5432 default in
+ * `external` mode (docker-compose's mapped port), or the Testcontainers-assigned host port in
+ * `testcontainers` mode.
*/
import {Client} from 'pg';
-const DEFAULT_DB_URL =
- 'postgres://mmuser:mostest@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10&binary_parameters=yes';
-
-function resolveDbUrl(): string {
- return process.env.MM_TEST_DB_URL ?? DEFAULT_DB_URL;
-}
+import {testConfig} from '@mattermost/playwright-lib';
async function runQuery(sql: string, params: unknown[] = []): Promise {
- const client = new Client({connectionString: resolveDbUrl()});
+ const client = new Client({connectionString: testConfig.postgresUrl});
await client.connect();
try {
const result = await client.query(sql, params);
diff --git a/e2e-tests/playwright/specs/functional/system_console/feature_flag.spec.ts b/e2e-tests/playwright/specs/functional/system_console/feature_flag.spec.ts
new file mode 100644
index 000000000000..305087780fa9
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/feature_flag.spec.ts
@@ -0,0 +1,22 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect, test} from '@mattermost/playwright-lib';
+
+/**
+ * @objective Verify a FeatureFlags entry can be switched via a server restart, since FeatureFlags
+ * are never re-read from a running server's config store and only take effect through a boot-time
+ * MM_FEATUREFLAGS_* env var.
+ *
+ * @precondition
+ * Full (Testcontainers) mode, so the server can be restarted with a different feature flag env var.
+ */
+test('flips a feature flag by restarting the server with it set', async ({pw}) => {
+ // Ensure prerequisites
+ await pw.ensureFeatureFlag('TestBoolFeature', true);
+
+ const {adminClient} = await pw.getAdminClient();
+ const config = await adminClient.getConfig();
+
+ expect(String(config.FeatureFlags?.TestBoolFeature)).toBe('true');
+});
diff --git a/e2e-tests/playwright/specs/functional/system_console/file_storage/azurite_file_storage.spec.ts b/e2e-tests/playwright/specs/functional/system_console/file_storage/azurite_file_storage.spec.ts
new file mode 100644
index 000000000000..c065b2079fb8
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/file_storage/azurite_file_storage.spec.ts
@@ -0,0 +1,22 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@mattermost/playwright-lib';
+
+import {uploadAndVerifyFile} from './file_storage_helpers';
+
+/**
+ * @objective Verify a file uploaded while FileSettings is configured for Azure Blob storage
+ * (against Azurite) can be uploaded and downloaded back through Mattermost.
+ *
+ * @precondition
+ * An Azurite instance reachable at the configured FileSettings.AzureEndpoint.
+ */
+test('uploads and retrieves a file backed by Azure Blob storage', async ({pw}) => {
+ // Ensure prerequisites
+ await pw.ensureAzurite();
+
+ const {userClient, team} = await pw.initSetup();
+
+ await uploadAndVerifyFile(userClient, team.id, () => pw.listAzuriteBlobNames());
+});
diff --git a/e2e-tests/playwright/specs/functional/system_console/file_storage/file_storage_helpers.ts b/e2e-tests/playwright/specs/functional/system_console/file_storage/file_storage_helpers.ts
new file mode 100644
index 000000000000..87dd6dd04318
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/file_storage/file_storage_helpers.ts
@@ -0,0 +1,47 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import type {Client4} from '@mattermost/client';
+
+import {expect, getFileFromAsset, getRandomId} from '@mattermost/playwright-lib';
+
+/**
+ * Uploads the shared test asset to the given team's town-square channel and downloads it back
+ * through Mattermost, asserting the round-tripped bytes match exactly. When `listObjectKeys` is
+ * provided (Minio/Azurite), also asserts the object landed in the backend's own listing —
+ * local storage has no such listing to check.
+ */
+export async function uploadAndVerifyFile(
+ userClient: Client4,
+ teamId: string,
+ listObjectKeys?: () => Promise,
+): Promise {
+ const townSquare = await userClient.getChannelByName(teamId, 'town-square');
+ const filename = 'mattermost-icon_128x128.png';
+ const file = getFileFromAsset(filename);
+
+ const formData = new FormData();
+ formData.set('channel_id', townSquare.id);
+ formData.set('client_ids', getRandomId());
+ formData.set('files', file, filename);
+
+ const objectKeysBeforeUpload = listObjectKeys ? await listObjectKeys() : undefined;
+
+ const uploadResponse = await userClient.uploadFile(formData);
+ const fileId = uploadResponse.file_infos[0].id;
+
+ if (listObjectKeys) {
+ const objectKeysAfterUpload = await listObjectKeys();
+ const newObjectKeys = objectKeysAfterUpload.filter((key) => !objectKeysBeforeUpload?.includes(key));
+ expect(newObjectKeys.length).toBeGreaterThan(0);
+ }
+
+ const downloadResponse = await fetch(userClient.getFileUrl(fileId, 0), {
+ headers: {Authorization: `Bearer ${userClient.getToken()}`},
+ });
+ expect(downloadResponse.ok).toBe(true);
+
+ const downloadedBytes = Buffer.from(await downloadResponse.arrayBuffer());
+ const originalBytes = Buffer.from(await file.arrayBuffer());
+ expect(downloadedBytes.equals(originalBytes)).toBe(true);
+}
diff --git a/e2e-tests/playwright/specs/functional/system_console/file_storage/local_file_storage.spec.ts b/e2e-tests/playwright/specs/functional/system_console/file_storage/local_file_storage.spec.ts
new file mode 100644
index 000000000000..a4de62be447f
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/file_storage/local_file_storage.spec.ts
@@ -0,0 +1,24 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@mattermost/playwright-lib';
+
+import {uploadAndVerifyFile} from './file_storage_helpers';
+
+/**
+ * @objective Verify a file uploaded while FileSettings is configured for local disk storage (the
+ * default) can be uploaded and downloaded back through Mattermost. Untagged, unlike the Minio/
+ * Azurite specs: local is already every core test's default backend, so ensureLocalFile() here is
+ * a no-op check rather than a disruptive restart, and this can run alongside normal core tests.
+ *
+ * @precondition
+ * None beyond the default server config.
+ */
+test('uploads and retrieves a file backed by local disk storage', async ({pw}) => {
+ // Ensure prerequisites
+ await pw.ensureLocalFile();
+
+ const {userClient, team} = await pw.initSetup();
+
+ await uploadAndVerifyFile(userClient, team.id);
+});
diff --git a/e2e-tests/playwright/specs/functional/system_console/file_storage/minio_file_storage.spec.ts b/e2e-tests/playwright/specs/functional/system_console/file_storage/minio_file_storage.spec.ts
new file mode 100644
index 000000000000..fe67bbe14ac8
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/file_storage/minio_file_storage.spec.ts
@@ -0,0 +1,22 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@mattermost/playwright-lib';
+
+import {uploadAndVerifyFile} from './file_storage_helpers';
+
+/**
+ * @objective Verify a file uploaded while FileSettings is configured for Minio S3-compatible
+ * storage is actually written to the Minio bucket and can be downloaded back through Mattermost.
+ *
+ * @precondition
+ * A Minio instance reachable at the configured FileSettings.AmazonS3Endpoint.
+ */
+test('uploads and retrieves a file backed by Minio S3 storage', async ({pw}) => {
+ // Ensure prerequisites
+ await pw.ensureMinio();
+
+ const {userClient, team} = await pw.initSetup();
+
+ await uploadAndVerifyFile(userClient, team.id, () => pw.listMinioObjectKeys());
+});
diff --git a/e2e-tests/playwright/specs/functional/system_console/ldap/ldap_login.spec.ts b/e2e-tests/playwright/specs/functional/system_console/ldap/ldap_login.spec.ts
new file mode 100644
index 000000000000..3ef69f543b82
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/ldap/ldap_login.spec.ts
@@ -0,0 +1,47 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect, test} from '@mattermost/playwright-lib';
+
+/**
+ * @objective Verify a user who only exists in the directory (never created in Mattermost) can
+ * authenticate through the standard login form once LDAP authentication is enabled, and that the
+ * server provisions their account via LDAP on first login rather than local auth.
+ *
+ * @precondition
+ * An LDAP directory reachable at the configured LdapSettings.LdapServer/LdapPort.
+ */
+test('logs in a directory-only user through the standard login form', {tag: '@ldap'}, async ({pw}) => {
+ // Ensure prerequisites
+ await pw.ensureLicense();
+ await pw.skipIfNoLicense();
+ await pw.ensureOpenldap();
+
+ const {adminClient} = await pw.getAdminClient();
+ const ldapUser = pw.generateLdapUser();
+ await pw.createLdapUser(ldapUser);
+
+ try {
+ // # Log in through the real login form with the directory-only user's credentials
+ await pw.hasSeenLandingPage();
+ await pw.loginPage.goto();
+ await pw.loginPage.toBeVisible();
+
+ // * Verify the login form reflects LDAP being enabled
+ await expect(pw.loginPage.loginWithAdLdapPlaceholder).toBeVisible();
+
+ await pw.loginPage.loginInput.fill(ldapUser.username);
+ await pw.loginPage.passwordInput.fill(ldapUser.password);
+ await pw.loginPage.signInButton.click();
+
+ // * Verify the login succeeded
+ await expect(pw.loginPage.page).not.toHaveURL(/\/login/);
+
+ // * Verify the server provisioned the account via LDAP, with the directory's attributes
+ const provisionedUser = await adminClient.getUserByUsername(ldapUser.username);
+ expect(provisionedUser.auth_service).toBe('ldap');
+ expect(provisionedUser.email).toBe(ldapUser.email);
+ } finally {
+ await pw.deleteLdapUser(ldapUser.username);
+ }
+});
diff --git a/e2e-tests/playwright/specs/functional/system_console/mmctl/mmctl_remote.spec.ts b/e2e-tests/playwright/specs/functional/system_console/mmctl/mmctl_remote.spec.ts
new file mode 100644
index 000000000000..acdacc22bedb
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/mmctl/mmctl_remote.spec.ts
@@ -0,0 +1,66 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect, test, getRandomId, newTestPassword} from '@mattermost/playwright-lib';
+
+/**
+ * @objective Verify mmctl — the same binary bundled in the server image — can administer the
+ * real test server as a remote client, run from a separate container instance built from that
+ * same image rather than the server's own container's `--local` unix socket.
+ *
+ * @precondition
+ * Full (Testcontainers) mode, so a Docker network and the server image are available to build a
+ * second, independent mmctl container instance from.
+ */
+test('creates a user through a remote mmctl container instance', {tag: '@mmctl'}, async ({pw}) => {
+ await pw.ensureMmctl();
+
+ const {adminClient} = await pw.getAdminClient();
+
+ const randomId = getRandomId();
+ const username = `mmctluser${randomId}`;
+ const email = `${username}@mmtest.com`;
+
+ // # Create a user via mmctl running in a separate remote container instance
+ const result = await pw.runMmctl([
+ 'user',
+ 'create',
+ '--username',
+ username,
+ '--email',
+ email,
+ '--password',
+ newTestPassword(),
+ ]);
+
+ // * Verify the mmctl command succeeded
+ expect(result.exitCode, result.output).toBe(0);
+
+ // * Verify the user was actually created on the real server
+ const createdUser = await adminClient.getUserByUsername(username);
+ expect(createdUser.email).toBe(email);
+});
+
+/**
+ * @objective Verify the remote mmctl container reports the same version as the server, since
+ * both come from the same image — a sanity check that the "different container/instance, same
+ * image" setup is actually using the image it claims to.
+ *
+ * @precondition
+ * Full (Testcontainers) mode, so a Docker network and the server image are available to build a
+ * second, independent mmctl container instance from.
+ */
+test('reports the same version as the server it is bundled with', {tag: '@mmctl'}, async ({pw}) => {
+ await pw.ensureMmctl();
+
+ const {adminClient} = await pw.getAdminClient();
+ const serverConfig = await adminClient.getClientConfig();
+
+ // # Query the version reported by the remote mmctl container instance
+ const result = await pw.runMmctl(['version', '--json']);
+ expect(result.exitCode, result.output).toBe(0);
+
+ // * Verify the remote mmctl reports the same version as the server it's bundled with
+ const [mmctlVersionInfo] = JSON.parse(result.output);
+ expect(mmctlVersionInfo.Version).toBe(serverConfig.Version);
+});
diff --git a/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts b/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts
index 563107d42121..044a13d7831d 100644
--- a/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts
+++ b/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {expect, test} from '@mattermost/playwright-lib';
+import {expect, test, testConfig} from '@mattermost/playwright-lib';
test('should be able to enable mobile security settings when licensed', async ({pw}) => {
const {adminUser, adminClient} = await pw.initSetup();
@@ -379,7 +379,7 @@ test('should configure new IntuneSettings with SAML auth provider', async ({pw})
}
// # Set server URL for fetch calls
- const serverUrl = process.env.MM_SERVER_URL || 'http://localhost:8065';
+ const serverUrl = testConfig.baseURL;
// # Upload a valid SAML IdP certificate using fetch
const idpCert =
diff --git a/e2e-tests/playwright/specs/functional/system_console/saml/saml_login.spec.ts b/e2e-tests/playwright/specs/functional/system_console/saml/saml_login.spec.ts
new file mode 100644
index 000000000000..62111768cb9e
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/saml/saml_login.spec.ts
@@ -0,0 +1,59 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect, test, getRandomId} from '@mattermost/playwright-lib';
+
+/**
+ * @objective Verify a user who only exists in Keycloak (never created in Mattermost) can
+ * authenticate through SAML SSO once SAML authentication is enabled, and that the server
+ * provisions their account via SAML on first login rather than local auth.
+ *
+ * @precondition
+ * A Keycloak realm reachable at the configured SamlSettings.IdpURL, with a SAML client matching
+ * ServiceProviderIdentifier.
+ */
+test('logs in a directory-only user through Keycloak SAML SSO', {tag: '@saml'}, async ({pw}) => {
+ // Ensure prerequisites
+ await pw.ensureLicense();
+ await pw.skipIfNoLicense();
+ await pw.ensureKeycloak();
+
+ const {adminClient} = await pw.getAdminClient();
+
+ const randomId = getRandomId();
+ const keycloakUser = {
+ username: `samluser${randomId}`,
+ email: `samluser${randomId}@mmtest.com`,
+ firstName: `Firstname-${randomId}`,
+ lastName: `Lastname-${randomId}`,
+ password: 'Password1',
+ };
+
+ const keycloakUserId = await pw.createKeycloakUser(keycloakUser);
+
+ try {
+ // # Log in through the SAML SSO button, which redirects to Keycloak's hosted login form
+ await pw.hasSeenLandingPage();
+ await pw.loginPage.goto();
+ await pw.loginPage.toBeVisible();
+
+ // * Verify the login form reflects SAML being enabled
+ await expect(pw.loginPage.samlLoginButton).toBeVisible();
+ await pw.loginPage.samlLoginButton.click();
+
+ // Keycloak's own hosted login form.
+ await pw.loginPage.page.locator('#username').fill(keycloakUser.username);
+ await pw.loginPage.page.locator('#password').fill(keycloakUser.password);
+ await pw.loginPage.page.locator('#kc-login').click();
+
+ // * Verify the login succeeded
+ await expect(pw.loginPage.page).not.toHaveURL(/\/login/);
+
+ // * Verify the server provisioned the account via SAML, with Keycloak's attributes
+ const provisionedUser = await adminClient.getUserByUsername(keycloakUser.username);
+ expect(provisionedUser.auth_service).toBe('saml');
+ expect(provisionedUser.email).toBe(keycloakUser.email);
+ } finally {
+ await pw.deleteKeycloakUser(keycloakUserId);
+ }
+});
diff --git a/e2e-tests/playwright/specs/functional/system_console/search/elasticsearch_search.spec.ts b/e2e-tests/playwright/specs/functional/system_console/search/elasticsearch_search.spec.ts
new file mode 100644
index 000000000000..297ace59652c
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/search/elasticsearch_search.spec.ts
@@ -0,0 +1,29 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@mattermost/playwright-lib';
+
+import {postAndFindMessage} from './search_helpers';
+
+/**
+ * @objective Verify a message posted while Elasticsearch indexing/searching is enabled can be
+ * found through the search API, confirming the server is actually querying Elasticsearch rather
+ * than silently falling back to database search.
+ *
+ * @precondition
+ * An Elasticsearch cluster reachable at the configured ElasticsearchSettings.ConnectionURL, and a
+ * server license that includes Elasticsearch.
+ */
+test('finds a newly posted message through Elasticsearch search', {tag: '@elasticsearch'}, async ({pw}) => {
+ // Ensure prerequisites
+ await pw.ensureLicense();
+ await pw.skipIfNoLicense();
+ await pw.ensureElasticsearch();
+
+ const {adminClient, userClient, team} = await pw.initSetup();
+ await adminClient.purgeElasticsearchIndexes();
+
+ // # Post a message with a unique term, then search for it via the search API
+ // * Verify the search API returns exactly the post containing the unique term
+ await postAndFindMessage(pw, userClient, team.id);
+});
diff --git a/e2e-tests/playwright/specs/functional/system_console/search/opensearch_search.spec.ts b/e2e-tests/playwright/specs/functional/system_console/search/opensearch_search.spec.ts
new file mode 100644
index 000000000000..88a780dda0c2
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/search/opensearch_search.spec.ts
@@ -0,0 +1,29 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@mattermost/playwright-lib';
+
+import {postAndFindMessage} from './search_helpers';
+
+/**
+ * @objective Verify a message posted while OpenSearch indexing/searching is enabled can be found
+ * through the search API, confirming the server is actually querying OpenSearch rather than
+ * silently falling back to database search.
+ *
+ * @precondition
+ * An OpenSearch cluster reachable at the configured ElasticsearchSettings.ConnectionURL, with
+ * Backend set to "opensearch", and a server license that includes Elasticsearch.
+ */
+test('finds a newly posted message through OpenSearch search', {tag: '@opensearch'}, async ({pw}) => {
+ // Ensure prerequisites
+ await pw.ensureLicense();
+ await pw.skipIfNoLicense();
+ await pw.ensureOpensearch();
+
+ const {adminClient, userClient, team} = await pw.initSetup();
+ await adminClient.purgeElasticsearchIndexes();
+
+ // # Post a message with a unique term, then search for it via the search API
+ // * Verify the search API returns exactly the post containing the unique term
+ await postAndFindMessage(pw, userClient, team.id);
+});
diff --git a/e2e-tests/playwright/specs/functional/system_console/search/postgres_search.spec.ts b/e2e-tests/playwright/specs/functional/system_console/search/postgres_search.spec.ts
new file mode 100644
index 000000000000..1e7a9739186e
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/search/postgres_search.spec.ts
@@ -0,0 +1,23 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {test} from '@mattermost/playwright-lib';
+
+import {postAndFindMessage} from './search_helpers';
+
+/**
+ * @objective Verify a message posted while Elasticsearch/OpenSearch indexing and searching are
+ * disabled can still be found through the search API, confirming the server falls back to
+ * database search.
+ *
+ * @precondition
+ * None beyond a running server — this is the default search mode.
+ */
+test('finds a newly posted message through database search', async ({pw}) => {
+ // Ensure prerequisites
+ await pw.ensurePostgresSearch();
+
+ const {userClient, team} = await pw.initSetup();
+
+ await postAndFindMessage(pw, userClient, team.id);
+});
diff --git a/e2e-tests/playwright/specs/functional/system_console/search/search_helpers.ts b/e2e-tests/playwright/specs/functional/system_console/search/search_helpers.ts
new file mode 100644
index 000000000000..0bf66239bbca
--- /dev/null
+++ b/e2e-tests/playwright/specs/functional/system_console/search/search_helpers.ts
@@ -0,0 +1,30 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import type {Client4} from '@mattermost/client';
+
+import {expect, getRandomId} from '@mattermost/playwright-lib';
+import type {PlaywrightExtended} from '@mattermost/playwright-lib';
+
+/**
+ * Posts a message with a unique term, polls the search API until it's indexed, then asserts it's
+ * the single result returned — the common flow across the Elasticsearch/OpenSearch/database search
+ * specs, which otherwise differ only in prerequisite setup (ensure*, plus index purge).
+ */
+export async function postAndFindMessage(pw: PlaywrightExtended, userClient: Client4, teamId: string): Promise {
+ const townSquare = await userClient.getChannelByName(teamId, 'town-square');
+ const uniqueTerm = `searchterm${getRandomId()}`;
+ await userClient.createPost({channel_id: townSquare.id, message: `hello ${uniqueTerm}`});
+
+ await pw.waitUntil(
+ async () => {
+ const results = await userClient.searchPosts(teamId, uniqueTerm, false);
+ return results.order.length > 0;
+ },
+ {timeout: 15000, intervalBetweenAttempts: 1000},
+ );
+
+ const results = await userClient.searchPosts(teamId, uniqueTerm, false);
+ expect(results.order).toHaveLength(1);
+ expect(results.posts[results.order[0]].message).toContain(uniqueTerm);
+}
diff --git a/server/Makefile b/server/Makefile
index 11f2982a45c0..ed09e559ef79 100644
--- a/server/Makefile
+++ b/server/Makefile
@@ -163,7 +163,7 @@ PLUGIN_PACKAGES += mattermost-plugin-jira-v4.7.1
PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.11.1
PLUGIN_PACKAGES += mattermost-plugin-servicenow-v2.4.0
PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.13.0
-PLUGIN_PACKAGES += mattermost-plugin-agents-v2.5.0-rc1
+PLUGIN_PACKAGES += mattermost-plugin-agents-v2.5.0-rc2
PLUGIN_PACKAGES += mattermost-plugin-boards-v9.3.1
PLUGIN_PACKAGES += mattermost-plugin-user-survey-v1.1.1
PLUGIN_PACKAGES += mattermost-plugin-mscalendar-v1.7.0
@@ -177,7 +177,7 @@ PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.3.0
# the way we pre-package FIPS and non-FIPS plugins.
ifeq ($(FIPS_ENABLED),true)
PLUGIN_PACKAGES = mattermost-plugin-playbooks-v2.11.1%2B329b65c-fips
- PLUGIN_PACKAGES += mattermost-plugin-agents-v2.5.0-rc1%2B666ae7a-fips
+ PLUGIN_PACKAGES += mattermost-plugin-agents-v2.5.0-rc2%2B2119bcf-fips
PLUGIN_PACKAGES += mattermost-plugin-boards-v9.3.1%2B29b4688-fips
endif
diff --git a/server/channels/app/channels.go b/server/channels/app/channels.go
index 07754530bd4c..ca7a478d4069 100644
--- a/server/channels/app/channels.go
+++ b/server/channels/app/channels.go
@@ -222,7 +222,8 @@ func NewChannels(s *Server) (*Channels, error) {
decoderConcurrency = runtime.NumCPU()
}
ch.imgDecoder, imgErr = imaging.NewDecoder(imaging.DecoderOptions{
- ConcurrencyLevel: decoderConcurrency,
+ ConcurrencyLevel: decoderConcurrency,
+ MaxDecodedResolution: *ch.cfgSvc.Config().FileSettings.MaxImageResolution,
})
if imgErr != nil {
return nil, errors.Wrap(imgErr, "failed to create image decoder")
diff --git a/server/channels/app/imaging/decode.go b/server/channels/app/imaging/decode.go
index 87bf4d37a3c6..95ed43826efb 100644
--- a/server/channels/app/imaging/decode.go
+++ b/server/channels/app/imaging/decode.go
@@ -4,6 +4,7 @@
package imaging
import (
+ "bytes"
"errors"
"fmt"
"image"
@@ -23,6 +24,13 @@ type DecoderOptions struct {
// The level of concurrency for the decoder. This defines a limit on the
// number of concurrently running encoding goroutines.
ConcurrencyLevel int
+
+ // MaxDecodedResolution, when greater than zero, is the maximum number of
+ // pixels (width*height) an image may declare before it is decoded. Images
+ // exceeding this limit are rejected up front. This is a defense-in-depth
+ // guard against decompression bombs that bounds server-side memory
+ // allocation regardless of the underlying codec's behavior.
+ MaxDecodedResolution int64
}
func (o *DecoderOptions) validate() error {
@@ -52,8 +60,76 @@ func NewDecoder(opts DecoderOptions) (*Decoder, error) {
return &d, nil
}
+// enforceResolutionLimit inspects the image header and rejects images whose
+// declared resolution exceeds the configured MaxDecodedResolution before any
+// pixel data is decoded. It returns the reader to use for the subsequent full
+// decode: seekable readers are rewound to their original position, while
+// non-seekable readers are buffered so the cap is enforced for every input.
+func (d *Decoder) enforceResolutionLimit(rd io.Reader) (io.Reader, error) {
+ if d.opts.MaxDecodedResolution <= 0 {
+ return rd, nil
+ }
+
+ if seeker, ok := rd.(io.ReadSeeker); ok {
+ // Preserve the caller's position so an image decoded from a non-zero
+ // offset still lines up for the full decode.
+ start, err := seeker.Seek(0, io.SeekCurrent)
+ if err != nil {
+ return nil, fmt.Errorf("imaging: failed to read image position: %w", err)
+ }
+ cfg, _, cfgErr := image.DecodeConfig(seeker)
+ if _, err := seeker.Seek(start, io.SeekStart); err != nil {
+ return nil, fmt.Errorf("imaging: failed to seek after reading image config: %w", err)
+ }
+ if err := d.checkConfigResolution(cfg, cfgErr); err != nil {
+ return nil, err
+ }
+ return rd, nil
+ }
+
+ // Non-seekable reader: buffer the input so the resolution cap can still be
+ // enforced and the data can be decoded afterwards.
+ data, err := io.ReadAll(rd)
+ if err != nil {
+ return nil, fmt.Errorf("imaging: failed to read image data: %w", err)
+ }
+ cfg, _, cfgErr := image.DecodeConfig(bytes.NewReader(data))
+ if err := d.checkConfigResolution(cfg, cfgErr); err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(data), nil
+}
+
+// checkConfigResolution rejects a decoded image config whose resolution exceeds
+// the configured cap. A config-decode error is ignored so the subsequent full
+// decode can surface a meaningful error for malformed input.
+func (d *Decoder) checkConfigResolution(cfg image.Config, cfgErr error) error {
+ if cfgErr != nil {
+ return nil
+ }
+ if exceedsResolution(int64(cfg.Width), int64(cfg.Height), d.opts.MaxDecodedResolution) {
+ return fmt.Errorf("imaging: image resolution %dx%d exceeds the maximum allowed %d pixels", cfg.Width, cfg.Height, d.opts.MaxDecodedResolution)
+ }
+ return nil
+}
+
+// exceedsResolution reports whether width*height exceeds maxRes. It divides
+// instead of multiplying so it can't overflow int64 for very large declared
+// dimensions.
+func exceedsResolution(width, height, maxRes int64) bool {
+ if width <= 0 || height <= 0 {
+ return false
+ }
+ return width > maxRes/height
+}
+
// Decode decodes the given encoded data and returns the decoded image.
func (d *Decoder) Decode(rd io.Reader) (img image.Image, format string, err error) {
+ rd, err = d.enforceResolutionLimit(rd)
+ if err != nil {
+ return nil, "", err
+ }
+
if d.opts.ConcurrencyLevel != 0 {
d.sem <- struct{}{}
defer func() { <-d.sem }()
@@ -71,6 +147,11 @@ func (d *Decoder) Decode(rd io.Reader) (img image.Image, format string, err erro
// must be called when access to the raw image is not needed anymore.
// This sets the raw image data pointer to nil in an attempt to help the GC to re-use the underlying data as soon as possible.
func (d *Decoder) DecodeMemBounded(rd io.Reader) (img image.Image, format string, releaseFunc func(), err error) {
+ rd, err = d.enforceResolutionLimit(rd)
+ if err != nil {
+ return nil, "", nil, err
+ }
+
if d.opts.ConcurrencyLevel != 0 {
d.sem <- struct{}{}
defer func() {
diff --git a/server/channels/app/imaging/decode_test.go b/server/channels/app/imaging/decode_test.go
index 5328f886e77e..2a873a2c79ac 100644
--- a/server/channels/app/imaging/decode_test.go
+++ b/server/channels/app/imaging/decode_test.go
@@ -5,6 +5,9 @@ package imaging
import (
"bytes"
+ "image"
+ "image/png"
+ "io"
"os"
"sync"
"testing"
@@ -256,3 +259,87 @@ func TestDecoderDecodeMemBounded(t *testing.T) {
require.Empty(t, d.sem)
})
}
+
+// TestDecoderMaxDecodedResolution verifies the defense-in-depth cap: the shared
+// decoder refuses to decode any image whose declared resolution exceeds the
+// configured limit, regardless of the underlying codec, before allocating
+// pixel data.
+func TestDecoderMaxDecodedResolution(t *testing.T) {
+ makePNG := func(w, h int) []byte {
+ var buf bytes.Buffer
+ require.NoError(t, png.Encode(&buf, image.NewRGBA(image.Rect(0, 0, w, h))))
+ return buf.Bytes()
+ }
+
+ d, err := NewDecoder(DecoderOptions{MaxDecodedResolution: 100})
+ require.NoError(t, err)
+
+ t.Run("Decode rejects image exceeding the cap", func(t *testing.T) {
+ img, format, decErr := d.Decode(bytes.NewReader(makePNG(50, 50))) // 2500px > 100
+ require.Error(t, decErr)
+ require.ErrorContains(t, decErr, "exceeds the maximum allowed")
+ require.Nil(t, img)
+ require.Empty(t, format)
+ })
+
+ t.Run("Decode allows image within the cap", func(t *testing.T) {
+ img, format, decErr := d.Decode(bytes.NewReader(makePNG(5, 5))) // 25px <= 100
+ require.NoError(t, decErr)
+ require.NotNil(t, img)
+ require.Equal(t, "png", format)
+ })
+
+ t.Run("DecodeMemBounded rejects image exceeding the cap", func(t *testing.T) {
+ img, format, release, decErr := d.DecodeMemBounded(bytes.NewReader(makePNG(50, 50)))
+ require.Error(t, decErr)
+ require.ErrorContains(t, decErr, "exceeds the maximum allowed")
+ require.Nil(t, img)
+ require.Empty(t, format)
+ require.Nil(t, release)
+ })
+
+ t.Run("cap disabled by default", func(t *testing.T) {
+ dd, ddErr := NewDecoder(DecoderOptions{})
+ require.NoError(t, ddErr)
+ img, _, decErr := dd.Decode(bytes.NewReader(makePNG(50, 50)))
+ require.NoError(t, decErr)
+ require.NotNil(t, img)
+ })
+
+ // A non-seekable reader must still be subject to the cap; the decoder
+ // buffers it internally rather than silently bypassing the check.
+ t.Run("cap enforced on non-seekable reader", func(t *testing.T) {
+ // io.MultiReader is not an io.ReadSeeker.
+ img, format, decErr := d.Decode(io.MultiReader(bytes.NewReader(makePNG(50, 50))))
+ require.Error(t, decErr)
+ require.ErrorContains(t, decErr, "exceeds the maximum allowed")
+ require.Nil(t, img)
+ require.Empty(t, format)
+ })
+
+ t.Run("non-seekable reader within cap decodes from buffer", func(t *testing.T) {
+ img, format, decErr := d.Decode(io.MultiReader(bytes.NewReader(makePNG(5, 5))))
+ require.NoError(t, decErr)
+ require.NotNil(t, img)
+ require.Equal(t, "png", format)
+ })
+}
+
+// TestExceedsResolution verifies the resolution comparison rejects over-limit
+// images (including dimensions large enough to overflow a naive int64
+// multiplication) without wrapping around.
+func TestExceedsResolution(t *testing.T) {
+ const maxRes = int64(7680 * 4320) // default 8K cap, ~33 MPx
+
+ require.False(t, exceedsResolution(100, 100, maxRes))
+ require.False(t, exceedsResolution(7680, 4320, maxRes)) // exactly at the cap
+ require.True(t, exceedsResolution(10000, 10000, maxRes))
+
+ // width*height here (2^80) overflows int64; the division-based check must
+ // still reject it rather than wrap to a small/negative value.
+ require.True(t, exceedsResolution(1<<40, 1<<40, maxRes))
+
+ // Non-positive dimensions are treated as not exceeding the cap.
+ require.False(t, exceedsResolution(0, 100, maxRes))
+ require.False(t, exceedsResolution(100, 0, maxRes))
+}
diff --git a/server/channels/app/server.go b/server/channels/app/server.go
index f2e7a7b67bfb..62a0eaf7ed05 100644
--- a/server/channels/app/server.go
+++ b/server/channels/app/server.go
@@ -524,6 +524,12 @@ func NewServer(options ...Option) (*Server, error) {
}
}
+ if cfg := s.platform.Config(); cfg.AccessControlSettings.EnableAccessControlAuditLogging != nil &&
+ *cfg.AccessControlSettings.EnableAccessControlAuditLogging &&
+ !config.IsAuditLoggingActive(cfg.ExperimentalAuditSettings, allowAdvancedLogging) {
+ mlog.Warn("AccessControlSettings.EnableAccessControlAuditLogging is enabled but no active audit log target is configured; ABAC policy-decision audit logging will have no effect. Enable ExperimentalAuditSettings.FileEnabled or configure an advanced audit logging target bound to an audit level.")
+ }
+
s.platform.RemoveUnlicensedLogTargets(license)
s.platform.EnableLoggingMetrics()
diff --git a/server/channels/app/team.go b/server/channels/app/team.go
index 1a4bdff03325..ae4e83aba80e 100644
--- a/server/channels/app/team.go
+++ b/server/channels/app/team.go
@@ -9,7 +9,6 @@ import (
"encoding/json"
"errors"
"fmt"
- "image"
"io"
"mime/multipart"
"net/http"
@@ -2354,8 +2353,10 @@ func (a *App) SetTeamIconFromMultiPartFile(rctx request.CTX, teamID string, file
}
func (a *App) SetTeamIconFromFile(rctx request.CTX, team *model.Team, file io.ReadSeeker) *model.AppError {
- // Decode image into Image object
- img, format, err := image.Decode(file)
+ // Decode image into Image object using the shared decoder so team icons
+ // are subject to the same concurrency and resolution safeguards as other
+ // user-uploaded images.
+ img, format, err := a.ch.imgDecoder.Decode(file)
if err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
diff --git a/server/config/client.go b/server/config/client.go
index 177f72323eb3..19e9892b4169 100644
--- a/server/config/client.go
+++ b/server/config/client.go
@@ -162,6 +162,8 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
props["EnableAttributeBasedAccessControl"] = strconv.FormatBool(*c.AccessControlSettings.EnableAttributeBasedAccessControl)
props["EnableUserManagedAttributes"] = strconv.FormatBool(*c.AccessControlSettings.EnableUserManagedAttributes)
+ props["EnableAccessControlAuditLogging"] = strconv.FormatBool(*c.AccessControlSettings.EnableAccessControlAuditLogging)
+ props["AuditLoggingActive"] = strconv.FormatBool(IsAuditLoggingActive(c.ExperimentalAuditSettings, license != nil && license.Features != nil && *license.Features.AdvancedLogging))
props["EnableChannelPolicyIndicators"] = strconv.FormatBool(*c.AccessControlSettings.EnableChannelPolicyIndicators)
props["WranglerPermittedWranglerRoles"] = strings.Join(c.WranglerSettings.PermittedWranglerRoles, ",")
diff --git a/server/config/client_test.go b/server/config/client_test.go
index 94ca13c64b14..6a4896b86a1d 100644
--- a/server/config/client_test.go
+++ b/server/config/client_test.go
@@ -4,6 +4,7 @@
package config
import (
+ "encoding/json"
"fmt"
"testing"
@@ -733,6 +734,81 @@ func TestGetClientConfig(t *testing.T) {
map[string]string{},
[]string{"MobileEphemeralModeEnabled", "MobileEphemeralModeDisconnectionTimeoutSeconds", "MobileEphemeralModeOfflinePersistenceTimerHours", "MobileEphemeralModeAutoCacheCleanupDays"},
},
+ {
+ "audit logging - default config",
+ &model.Config{},
+ "",
+ nil,
+ map[string]string{
+ "EnableAccessControlAuditLogging": "false",
+ "AuditLoggingActive": "false",
+ },
+ []string{},
+ },
+ {
+ "audit logging - file audit enabled",
+ &model.Config{
+ ExperimentalAuditSettings: model.ExperimentalAuditSettings{
+ FileEnabled: new(true),
+ FileName: new("audit.log"),
+ },
+ },
+ "",
+ nil,
+ map[string]string{
+ "EnableAccessControlAuditLogging": "false",
+ "AuditLoggingActive": "true",
+ },
+ []string{},
+ },
+ {
+ "audit logging - setting enabled independent of active state",
+ &model.Config{
+ AccessControlSettings: model.AccessControlSettings{
+ EnableAccessControlAuditLogging: new(true),
+ },
+ },
+ "",
+ nil,
+ map[string]string{
+ "EnableAccessControlAuditLogging": "true",
+ "AuditLoggingActive": "false",
+ },
+ []string{},
+ },
+ {
+ "audit logging - advanced target with license",
+ &model.Config{
+ ExperimentalAuditSettings: model.ExperimentalAuditSettings{
+ AdvancedLoggingJSON: json.RawMessage(`{"my-audit":{"type":"file","levels":[{"id":100,"name":"audit-api"}],"options":{"filename":"audit.log"}}}`),
+ },
+ },
+ "",
+ &model.License{
+ Features: &model.Features{
+ AdvancedLogging: model.NewPointer(true),
+ },
+ SkuShortName: model.LicenseShortSkuEnterprise,
+ },
+ map[string]string{
+ "AuditLoggingActive": "true",
+ },
+ []string{},
+ },
+ {
+ "audit logging - advanced target without license",
+ &model.Config{
+ ExperimentalAuditSettings: model.ExperimentalAuditSettings{
+ AdvancedLoggingJSON: json.RawMessage(`{"my-audit":{"type":"file","levels":[{"id":100,"name":"audit-api"}],"options":{"filename":"audit.log"}}}`),
+ },
+ },
+ "",
+ nil,
+ map[string]string{
+ "AuditLoggingActive": "false",
+ },
+ []string{},
+ },
{
"notification metrics enabled follows the metrics setting",
&model.Config{
diff --git a/server/config/logger.go b/server/config/logger.go
index 53ac714ddcf4..6428c4607ff2 100644
--- a/server/config/logger.go
+++ b/server/config/logger.go
@@ -10,6 +10,8 @@ import (
"path/filepath"
"strings"
+ "github.com/mattermost/logr/v2"
+
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/utils"
@@ -97,6 +99,47 @@ func MloggerConfigFromAuditConfig(auditSettings model.ExperimentalAuditSettings,
return cfg, nil
}
+// auditLevelIDs is the set of mlog level IDs that represent audit output
+// (audit-api, audit-content, audit-permissions, audit-cli).
+var auditLevelIDs = func() map[logr.LevelID]struct{} {
+ m := make(map[logr.LevelID]struct{}, len(mlog.MLvlAuditAll))
+ for _, l := range mlog.MLvlAuditAll {
+ m[l.ID] = struct{}{}
+ }
+ return m
+}()
+
+// IsAuditLoggingActive reports whether the server is actually emitting audit logs to at
+// least one sink, given the audit settings and whether advanced logging is licensed.
+//
+// It returns true when basic file auditing is enabled, or when the advanced audit logging
+// config defines at least one target bound to an audit level. A valid advanced-logging
+// config that routes nothing to an audit level returns false.
+func IsAuditLoggingActive(auditSettings model.ExperimentalAuditSettings, allowAdvancedLogging bool) bool {
+ if auditSettings.FileEnabled != nil && *auditSettings.FileEnabled {
+ return true
+ }
+
+ if !allowAdvancedLogging {
+ return false
+ }
+
+ cfg := make(mlog.LoggerConfiguration)
+ if err := json.Unmarshal(auditSettings.GetAdvancedLoggingConfig(), &cfg); err != nil {
+ return false
+ }
+
+ for _, target := range cfg {
+ for _, level := range target.Levels {
+ if _, ok := auditLevelIDs[level.ID]; ok {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
func GetLogFileLocation(fileLocation string) string {
if fileLocation == "" {
fileLocation, _ = fileutils.FindDir("logs")
diff --git a/server/config/logger_test.go b/server/config/logger_test.go
index d817559156db..ffdb19c30d45 100644
--- a/server/config/logger_test.go
+++ b/server/config/logger_test.go
@@ -213,3 +213,72 @@ func TestValidateLogFilePath(t *testing.T) {
assert.NoError(t, err)
})
}
+
+func TestIsAuditLoggingActive(t *testing.T) {
+ auditTargetJSON := json.RawMessage(`{"my-audit":{"type":"file","levels":[{"id":100,"name":"audit-api"}],"options":{"filename":"audit.log"}}}`)
+ stdTargetJSON := json.RawMessage(`{"my-log":{"type":"file","levels":[{"id":4,"name":"info"}],"options":{"filename":"info.log"}}}`)
+ malformedJSON := json.RawMessage(`{not valid json`)
+
+ tests := []struct {
+ name string
+ fileEnabled bool
+ fileName string
+ advancedLoggingJSON json.RawMessage
+ allowAdvancedLogging bool
+ expected bool
+ }{
+ {
+ name: "A - file audit enabled (license independent)",
+ fileEnabled: true,
+ fileName: "audit.log",
+ allowAdvancedLogging: false,
+ expected: true,
+ },
+ {
+ name: "B - advanced audit target, licensed",
+ fileEnabled: false,
+ advancedLoggingJSON: auditTargetJSON,
+ allowAdvancedLogging: true,
+ expected: true,
+ },
+ {
+ name: "B - advanced audit target, unlicensed",
+ fileEnabled: false,
+ advancedLoggingJSON: auditTargetJSON,
+ allowAdvancedLogging: false,
+ expected: false,
+ },
+ {
+ name: "C - valid advanced config, no audit level",
+ fileEnabled: false,
+ advancedLoggingJSON: stdTargetJSON,
+ allowAdvancedLogging: true,
+ expected: false,
+ },
+ {
+ name: "D - nothing configured",
+ fileEnabled: false,
+ allowAdvancedLogging: true,
+ expected: false,
+ },
+ {
+ name: "malformed advanced JSON",
+ fileEnabled: false,
+ advancedLoggingJSON: malformedJSON,
+ allowAdvancedLogging: true,
+ expected: false,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ auditSettings := model.ExperimentalAuditSettings{}
+ auditSettings.SetDefaults()
+ *auditSettings.FileEnabled = tc.fileEnabled
+ *auditSettings.FileName = tc.fileName
+ auditSettings.AdvancedLoggingJSON = tc.advancedLoggingJSON
+
+ assert.Equal(t, tc.expected, IsAuditLoggingActive(auditSettings, tc.allowAdvancedLogging))
+ })
+ }
+}
diff --git a/server/go.mod b/server/go.mod
index 213762c3daa2..a36ef1d4c859 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -78,12 +78,12 @@ require (
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c
github.com/yuin/goldmark v1.8.2
golang.org/x/crypto v0.53.0
- golang.org/x/image v0.40.0
+ golang.org/x/image v0.44.0
golang.org/x/net v0.56.0
- golang.org/x/sync v0.21.0
+ golang.org/x/sync v0.22.0
golang.org/x/sys v0.46.0
golang.org/x/term v0.44.0
- golang.org/x/text v0.38.0
+ golang.org/x/text v0.40.0
)
require (
@@ -220,8 +220,8 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect
- golang.org/x/mod v0.36.0 // indirect
- golang.org/x/tools v0.45.0 // indirect
+ golang.org/x/mod v0.37.0 // indirect
+ golang.org/x/tools v0.47.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect
google.golang.org/grpc v1.81.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
diff --git a/server/go.sum b/server/go.sum
index c917787a9297..9c3e4bb9887d 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -716,8 +716,8 @@ golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsi
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
-golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
-golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
+golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
+golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -727,8 +727,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
-golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
+golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
+golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -776,8 +776,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
-golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
+golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -839,8 +839,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
-golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
+golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
+golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
@@ -856,8 +856,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
-golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
+golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
+golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/server/public/model/config.go b/server/public/model/config.go
index 7fcf6566949c..7c3bb59fd114 100644
--- a/server/public/model/config.go
+++ b/server/public/model/config.go
@@ -4110,6 +4110,7 @@ type AccessControlSettings struct {
EnableChannelPolicyIndicators *bool `access:"write_restrictable"`
TrustProxyDeviceIdentityHeader *bool `access:"write_restrictable,cloud_restrictable"`
EnforceDeviceIDConsistency *bool `access:"write_restrictable,cloud_restrictable"`
+ EnableAccessControlAuditLogging *bool `access:"write_restrictable,cloud_restrictable"`
}
func (s *AccessControlSettings) SetDefaults() {
@@ -4134,6 +4135,10 @@ func (s *AccessControlSettings) SetDefaults() {
if s.EnforceDeviceIDConsistency == nil {
s.EnforceDeviceIDConsistency = new(false)
}
+
+ if s.EnableAccessControlAuditLogging == nil {
+ s.EnableAccessControlAuditLogging = new(false)
+ }
}
type ConfigFunc func() *Config
diff --git a/server/public/model/config_test.go b/server/public/model/config_test.go
index bfe280ab9f0b..513a4b888ba6 100644
--- a/server/public/model/config_test.go
+++ b/server/public/model/config_test.go
@@ -70,6 +70,12 @@ func TestConfigDefaults(t *testing.T) {
require.Equal(t, "", *c.SupportSettings.ReportAProblemMail)
require.Equal(t, true, *c.SupportSettings.AllowDownloadLogs)
})
+ t.Run("access control audit logging default", func(t *testing.T) {
+ c := Config{}
+ c.SetDefaults()
+ require.NotNil(t, c.AccessControlSettings.EnableAccessControlAuditLogging)
+ require.False(t, *c.AccessControlSettings.EnableAccessControlAuditLogging)
+ })
}
func TestConfigIsValid(t *testing.T) {
diff --git a/webapp/channels/src/components/__snapshots__/size_aware_image.test.tsx.snap b/webapp/channels/src/components/__snapshots__/size_aware_image.test.tsx.snap
index 45af7f432774..d9c9efb84559 100644
--- a/webapp/channels/src/components/__snapshots__/size_aware_image.test.tsx.snap
+++ b/webapp/channels/src/components/__snapshots__/size_aware_image.test.tsx.snap
@@ -12,7 +12,6 @@ exports[`components/SizeAwareImage should load download and copy link buttons wh
class="image-loading__container"
>
({
+ ...jest.requireActual('mattermost-redux/selectors/entities/general'),
+ getConfig: jest.fn(),
+}));
+
+const mockedGetConfig = getConfig as jest.Mock;
+
+type BoolAdminDefinitionSetting = AdminDefinitionSettingInput;
+
+const enterpriseAdvancedLicense = {
+ IsLicensed: 'true',
+ SkuShortName: LicenseSkus.EnterpriseAdvanced,
+} as ClientLicense;
+
+const professionalLicense = {
+ IsLicensed: 'true',
+ SkuShortName: LicenseSkus.Professional,
+} as ClientLicense;
+
+const consoleAccess = {
+ read: {},
+ write: {},
+} as ConsoleAccess;
+
+const abacFeatureFlagEnabled = {
+ FeatureFlags: {
+ AttributeBasedAccessControl: true,
+ },
+} as unknown as Partial;
+
+function getAuditLoggingSetting(): BoolAdminDefinitionSetting {
+ const subsection = AdminDefinition.system_attributes.subsections.attribute_based_access_control;
+ const schema = subsection.schema;
+ const sections = 'sections' in schema ? schema.sections ?? [] : [];
+ const settings = sections[0]?.settings ?? [];
+ const setting = settings.find((s) => s.key === 'AccessControlSettings.EnableAccessControlAuditLogging');
+ return setting as BoolAdminDefinitionSetting;
+}
+
+function callIsDisabled(check: Check | undefined, state: Record) {
+ const disabledCheck = check as Extract boolean>;
+ return disabledCheck({}, state, enterpriseAdvancedLicense, true, consoleAccess);
+}
+
+describe('AdminDefinition - ABAC audit logging toggle', () => {
+ afterEach(() => {
+ mockedGetConfig.mockReset();
+ });
+
+ test('defines the EnableAccessControlAuditLogging bool setting with the expected copy', () => {
+ const setting = getAuditLoggingSetting();
+
+ expect(setting).toBeDefined();
+ expect(setting.type).toBe('bool');
+ expect(setting.key).toBe('AccessControlSettings.EnableAccessControlAuditLogging');
+ expect(setting.label).toBeDefined();
+ expect((setting.label as {id: string}).id).toBe('admin.accesscontrol.enableAuditLogging.title');
+ expect(setting.help_text).toBeDefined();
+ expect((setting.help_text as {id: string}).id).toBe('admin.accesscontrol.enableAuditLogging.desc');
+ expect(setting.disabled_help_text).toBeDefined();
+ expect((setting.disabled_help_text as {id: string}).id).toBe('admin.accesscontrol.enableAuditLogging.disabled');
+ });
+
+ test('is enabled when ABAC is on and audit logging is active', () => {
+ mockedGetConfig.mockReturnValue({AuditLoggingActive: 'true'});
+
+ const setting = getAuditLoggingSetting();
+ const disabled = callIsDisabled(setting.isDisabled, {'AccessControlSettings.EnableAttributeBasedAccessControl': true});
+
+ expect(disabled).toBe(false);
+ });
+
+ test('is disabled when ABAC master toggle is off', () => {
+ mockedGetConfig.mockReturnValue({AuditLoggingActive: 'true'});
+
+ const setting = getAuditLoggingSetting();
+ const disabled = callIsDisabled(setting.isDisabled, {'AccessControlSettings.EnableAttributeBasedAccessControl': false});
+
+ expect(disabled).toBe(true);
+ });
+
+ test('is disabled when server audit logging is not active', () => {
+ mockedGetConfig.mockReturnValue({AuditLoggingActive: 'false'});
+
+ const setting = getAuditLoggingSetting();
+ const disabled = callIsDisabled(setting.isDisabled, {'AccessControlSettings.EnableAttributeBasedAccessControl': true});
+
+ expect(disabled).toBe(true);
+ });
+
+ test('is disabled when AuditLoggingActive is undefined (fail-safe default)', () => {
+ mockedGetConfig.mockReturnValue({});
+
+ const setting = getAuditLoggingSetting();
+ const disabled = callIsDisabled(setting.isDisabled, {'AccessControlSettings.EnableAttributeBasedAccessControl': true});
+
+ expect(disabled).toBe(true);
+ });
+
+ test('subsection is hidden below Enterprise Advanced license tier', () => {
+ const subsection = AdminDefinition.system_attributes.subsections.attribute_based_access_control;
+ const hiddenCheck = subsection.isHidden as Extract boolean>;
+
+ const readAccess = {
+ read: {[RESOURCE_KEYS.USER_MANAGEMENT.SYSTEM_ROLES]: true},
+ } as unknown as ConsoleAccess;
+
+ expect(hiddenCheck(abacFeatureFlagEnabled, {}, enterpriseAdvancedLicense, true, readAccess)).toBe(false);
+ expect(hiddenCheck(abacFeatureFlagEnabled, {}, professionalLicense, true, readAccess)).toBe(true);
+ });
+});
diff --git a/webapp/channels/src/components/admin_console/admin_definition_helpers.tsx b/webapp/channels/src/components/admin_console/admin_definition_helpers.tsx
index e75fa3818a80..efeb0223d4fa 100644
--- a/webapp/channels/src/components/admin_console/admin_definition_helpers.tsx
+++ b/webapp/channels/src/components/admin_console/admin_definition_helpers.tsx
@@ -5,7 +5,11 @@ import React from 'react';
import {defineMessage, type MessageDescriptor} from 'react-intl';
import type {CloudState, Product} from '@mattermost/types/cloud';
-import type {AdminConfig, ClientLicense} from '@mattermost/types/config';
+import type {AdminConfig, ClientConfig, ClientLicense} from '@mattermost/types/config';
+
+import {getConfig as getClientConfig} from 'mattermost-redux/selectors/entities/general';
+
+import store from 'stores/redux_store';
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
@@ -45,6 +49,8 @@ export const it = {
stateIsFalse: (key: string) => (config: Partial, state: any) => !state[key],
configIsTrue: (group: keyof Partial, setting: string) => (config: Partial) => Boolean((config[group] as any)?.[setting]),
configIsFalse: (group: keyof Partial, setting: string) => (config: Partial) => !(config[group] as any)?.[setting],
+ clientConfigIsTrue: (setting: keyof ClientConfig) => () => getClientConfig(store.getState())[setting] === 'true',
+ clientConfigIsFalse: (setting: keyof ClientConfig) => () => getClientConfig(store.getState())[setting] !== 'true',
configContains: (group: keyof Partial, setting: string, word: string) => (config: Partial) => Boolean((config[group] as any)?.[setting]?.includes(word)),
enterpriseReady: (config: Partial, state: any, license?: ClientLicense, enterpriseReady?: boolean) => Boolean(enterpriseReady),
licensed: (config: Partial, state: any, license?: ClientLicense) => license?.IsLicensed === 'true',
diff --git a/webapp/channels/src/components/admin_console/permission_policies/policy_details/permission_policy_details.test.tsx b/webapp/channels/src/components/admin_console/permission_policies/policy_details/permission_policy_details.test.tsx
index ad0f92c6b5e3..c7c2994e63a5 100644
--- a/webapp/channels/src/components/admin_console/permission_policies/policy_details/permission_policy_details.test.tsx
+++ b/webapp/channels/src/components/admin_console/permission_policies/policy_details/permission_policy_details.test.tsx
@@ -82,6 +82,7 @@ describe('components/admin_console/permission_policies/policy_details/Permission
EnableChannelPolicyIndicators: true,
TrustProxyDeviceIdentityHeader: false,
EnforceDeviceIDConsistency: false,
+ EnableAccessControlAuditLogging: false,
};
const baseProps = {
diff --git a/webapp/channels/src/components/markdown_image/__snapshots__/markdown_image.test.tsx.snap b/webapp/channels/src/components/markdown_image/__snapshots__/markdown_image.test.tsx.snap
index 8deee93d9b63..4adc079fa51b 100644
--- a/webapp/channels/src/components/markdown_image/__snapshots__/markdown_image.test.tsx.snap
+++ b/webapp/channels/src/components/markdown_image/__snapshots__/markdown_image.test.tsx.snap
@@ -12,7 +12,6 @@ exports[`components/MarkdownImage should match snapshot 1`] = `
class="image-loading__container"
>
{
} = this.props;
const renderPlaceholderOnly = this.props.renderPlaceholderOnly ?? false;
- let ariaLabelImage = this.props.intl.formatMessage({id: 'file_attachment.thumbnail', defaultMessage: 'file thumbnail'});
- if (fileInfo) {
- ariaLabelImage += ` ${fileInfo.name}`.toLowerCase();
- }
-
let fallback;
if (this.dimensionsAvailable(dimensions) && (!this.state.loaded || renderPlaceholderOnly)) {
@@ -432,7 +427,6 @@ export class SizeAwareImage extends React.PureComponent {
{this.renderImageLoaderIfNeeded()}
{
EnableChannelPolicyIndicators: true,
TrustProxyDeviceIdentityHeader: false,
EnforceDeviceIDConsistency: false,
+ EnableAccessControlAuditLogging: false,
},
areThereUnsavedChanges: false,
setAreThereUnsavedChanges: jest.fn(),
diff --git a/webapp/channels/src/components/team_settings/team_access_policies_tab/team_policy_editor.test.tsx b/webapp/channels/src/components/team_settings/team_access_policies_tab/team_policy_editor.test.tsx
index 9babb56cdb98..c34ea7f7c37f 100644
--- a/webapp/channels/src/components/team_settings/team_access_policies_tab/team_policy_editor.test.tsx
+++ b/webapp/channels/src/components/team_settings/team_access_policies_tab/team_policy_editor.test.tsx
@@ -51,6 +51,7 @@ describe('TeamPolicyEditor', () => {
EnableChannelPolicyIndicators: true,
TrustProxyDeviceIdentityHeader: false,
EnforceDeviceIDConsistency: false,
+ EnableAccessControlAuditLogging: false,
},
onNavigateBack: jest.fn(),
actions: {
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index 953f3ddfb955..e11fab2bc630 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -472,6 +472,9 @@
"admin.access_control.table_editor.values.create_placeholder": "Type to create value",
"admin.access_control.table_editor.values.select_values": "Select values...",
"admin.access_control.testResults": "Access Rule Test Results",
+ "admin.accesscontrol.enableAuditLogging.desc": "When enabled, attribute-based access control policy decisions are written to the server audit log. Requires server audit logging to be active.",
+ "admin.accesscontrol.enableAuditLogging.disabled": "When enabled, attribute-based access control policy decisions are written to the server audit log. This setting requires attribute-based access control to be enabled and server audit logging to be active (enable file audit logging or configure an advanced audit logging target).",
+ "admin.accesscontrol.enableAuditLogging.title": "Enable audit logging for access control decisions",
"admin.accesscontrol.enableChannelPolicyIndicatorsDesc": "When enabled, channels restricted by a membership access policy display the matching user attributes as tags in the channel members list and the invite dialog. Disable this to avoid revealing policy details to end users.",
"admin.accesscontrol.enableChannelPolicyIndicatorsTitle": "Show channel access indicators to end users",
"admin.accesscontrol.enableDesc": "Allow access restrictions based on user attributes using custom access policies. To effectively use this feature, you must define user attributes in the {userAttributes} section.",
diff --git a/webapp/platform/types/src/config.ts b/webapp/platform/types/src/config.ts
index 73caae5a73d3..5a08a35a17b7 100644
--- a/webapp/platform/types/src/config.ts
+++ b/webapp/platform/types/src/config.ts
@@ -249,6 +249,8 @@ export type ClientConfig = {
// Access Control Settings
EnableAttributeBasedAccessControl: string;
EnableUserManagedAttributes: string;
+ EnableAccessControlAuditLogging: string;
+ AuditLoggingActive: string;
EnableChannelPolicyIndicators: string;
// Auto Translation Settings
@@ -1066,6 +1068,7 @@ export type AccessControlSettings = {
EnableChannelPolicyIndicators: boolean;
TrustProxyDeviceIdentityHeader: boolean;
EnforceDeviceIDConsistency: boolean;
+ EnableAccessControlAuditLogging: boolean;
};
export type ContentFlaggingNotificationSettings = {