diff --git a/.env.template b/.env.template index fd7c2fd2..9fc29989 100644 --- a/.env.template +++ b/.env.template @@ -316,6 +316,14 @@ ## unauthenticated access to potentially sensitive data. # SHOW_PASSWORD_HINT=false +######################### +### Client settings ### +######################### + +## Control whether clients onboarding interstitials are suppressed +## (post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals) +# CLIENT_SUPPRESS_ONBOARDING=false + ######################### ### Advanced settings ### ######################### diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 17151922..3111e20b 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -41,12 +41,12 @@ jobs: # Uses the Docker-based action (hadolint pre-bundled in ghcr.io/hadolint/hadolint:v2.14.0-debian) # so no binary is downloaded at runtime. Pinned by commit SHA for supply-chain safety. - name: Run hadolint on Dockerfile.debian - uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 + uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0 with: dockerfile: docker/Dockerfile.debian - name: Run hadolint on Dockerfile.alpine - uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 + uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0 with: dockerfile: docker/Dockerfile.alpine # End Test Dockerfiles with hadolint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0efd21db..9d15dd88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -121,7 +121,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -137,7 +137,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -237,7 +237,7 @@ jobs: # Upload artifacts to Github Actions and Attest the binaries - name: Attest binaries - uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-path: vaultwarden-${{ env.NORMALIZED_ARCH }} @@ -272,7 +272,7 @@ jobs: # Login to Docker Hub - name: Login to Docker Hub - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -287,7 +287,7 @@ jobs: # Login to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -303,7 +303,7 @@ jobs: # Login to Quay.io - name: Login to Quay.io - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -365,7 +365,7 @@ jobs: # Attest container images - name: Attest - docker.io - ${{ matrix.base_image }} if: ${{ vars.DOCKERHUB_REPO != '' && env.DIGEST_SHA != ''}} - uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: ${{ vars.DOCKERHUB_REPO }} subject-digest: ${{ env.DIGEST_SHA }} @@ -373,7 +373,7 @@ jobs: - name: Attest - ghcr.io - ${{ matrix.base_image }} if: ${{ vars.GHCR_REPO != '' && env.DIGEST_SHA != ''}} - uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: ${{ vars.GHCR_REPO }} subject-digest: ${{ env.DIGEST_SHA }} @@ -381,7 +381,7 @@ jobs: - name: Attest - quay.io - ${{ matrix.base_image }} if: ${{ vars.QUAY_REPO != '' && env.DIGEST_SHA != ''}} - uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: ${{ vars.QUAY_REPO }} subject-digest: ${{ env.DIGEST_SHA }} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index c1f42c56..942a99e9 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -50,6 +50,6 @@ jobs: severity: CRITICAL,HIGH - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 7c345e0a..779cd6e3 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -23,4 +23,4 @@ jobs: # When this version is updated, do not forget to update this in `.pre-commit-config.yaml` too - name: Spell Check Repo - uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0 + uses: crate-ci/typos@8a48f81b6c64dcfea44b3633223084c4be58ac5f # v1.49.0 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 72810c67..e1de58c3 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1 + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 with: # intentionally not scanning the entire repository, # since it contains integration tests. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 35a0140e..f9920696 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,9 +18,10 @@ repos: # When this version is updated, do not forget to update this in `.github/workflows/typos.yaml` too - repo: https://github.com/crate-ci/typos - rev: bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0 + rev: 8a48f81b6c64dcfea44b3633223084c4be58ac5f # v1.49.0 hooks: - id: typos + always_run: true - repo: local hooks: @@ -38,8 +39,7 @@ repos: entry: cargo test language: system args: [ "--features", "sqlite,mysql,postgresql", "--" ] - types_or: [ rust, file ] - files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$) + types_or: [ rust, toml ] # Cargo.lock matches toml type which is intended pass_filenames: false - id: cargo-clippy name: cargo clippy @@ -47,8 +47,7 @@ repos: entry: cargo clippy language: system args: [ "--features", "sqlite,mysql,postgresql", "--", "-D", "warnings" ] - types_or: [ rust, file ] - files: (Cargo.toml|Cargo.lock|rust-toolchain.toml|rustfmt.toml|.*\.rs$) + types_or: [ rust, toml ] # Cargo.lock matches toml type which is intended pass_filenames: false - id: check-docker-templates name: check docker templates diff --git a/Cargo.lock b/Cargo.lock index 6b20797b..21defe79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -52,9 +52,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -150,9 +150,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -213,7 +213,7 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener 5.4.1", + "event-listener 5.4.2", "event-listener-strategy", "pin-project-lite", ] @@ -231,7 +231,7 @@ dependencies = [ "async-task", "blocking", "cfg-if", - "event-listener 5.4.1", + "event-listener 5.4.2", "futures-lite", "rustix", ] @@ -349,9 +349,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-config" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "701418aa459dac33e50a0f8e818e5662a16bc018a6ac7423659b70f3799d67a8" +checksum = "1b180a3c8b55960db3426d8964b8745e652466a1a49fe1a2eda828046d30b5e4" dependencies = [ "aws-credential-types", "aws-runtime", @@ -369,7 +369,7 @@ dependencies = [ "bytes", "fastrand", "hex", - "http 1.4.2", + "http 1.5.0", "sha1 0.10.7", "time", "tokio", @@ -392,9 +392,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6b50a43f3ccdf331521c6d6c68b7cc9668b6e09d439ebda9569df5722324d76" +checksum = "c9007227e10b5fed2f3e0a2beff489211e2b5604c400b7a9d5d81ca9d64c24bb" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -407,7 +407,7 @@ dependencies = [ "bytes", "bytes-utils", "fastrand", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "percent-encoding", "pin-project-lite", @@ -417,9 +417,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.104.0" +version = "1.105.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53416d16c278234845392e38d93bd4481d2f09daa0f005a2277f0aa91f59c22" +checksum = "6ffd0fbe7873cb548a7aa60f9573c268fff94155397fd4f14dc9f1ecaaab8516" dependencies = [ "arc-swap", "aws-credential-types", @@ -436,16 +436,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.106.0" +version = "1.107.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9b706c3305ed0285d5b1b696c747aa34950f830fb03e3e6c76890f99b9f188" +checksum = "175763eb222a46377df7aa257a3bca980ab3e96703fefc8f4d0b8da6ad2e254c" dependencies = [ "arc-swap", "aws-credential-types", @@ -462,16 +462,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.109.0" +version = "1.110.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d214cdfa5bbe17f117e76a7643fadf32a5234fb597322ef8b1fb4b2f17dbbd" +checksum = "dd8b14781dfbff48984017d57167b6ea0b6471c6920ec52b44a2677c7feb3c13" dependencies = [ "arc-swap", "aws-credential-types", @@ -489,7 +489,7 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "regex-lite", "tracing", ] @@ -509,7 +509,7 @@ dependencies = [ "hex", "hmac 0.13.0", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "percent-encoding", "sha2 0.11.0", "time", @@ -539,7 +539,7 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "percent-encoding", @@ -596,7 +596,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "http-body 0.4.6", "http-body 1.1.0", "http-body-util", @@ -617,7 +617,7 @@ dependencies = [ "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "pin-project-lite", "tokio", "tracing", @@ -643,7 +643,7 @@ checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", - "http 1.4.2", + "http 1.5.0", ] [[package]] @@ -656,7 +656,7 @@ dependencies = [ "bytes", "bytes-utils", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "http-body 0.4.6", "http-body 1.1.0", "http-body-util", @@ -714,6 +714,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64-simd" version = "0.8.0" @@ -1129,12 +1135,13 @@ dependencies = [ ] [[package]] -name = "crc32c" -version = "0.6.8" +name = "crc-fast" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" dependencies = [ - "rustc_version", + "digest 0.10.7", + "spin 0.10.1", ] [[package]] @@ -1154,13 +1161,14 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "cron" -version = "0.15.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" +checksum = "a5dcd6f69605c2956916ce24e8af637b754964c9a83f4662d3a2361654cdba09" dependencies = [ "chrono", "once_cell", - "winnow 0.6.26", + "phf 0.11.3", + "winnow 0.7.15", ] [[package]] @@ -1380,9 +1388,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "data-url" @@ -1638,13 +1646,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -1737,9 +1745,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "elliptic-curve" @@ -1764,11 +1772,11 @@ dependencies = [ [[package]] name = "email-encoding" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +checksum = "420b9da095f052ea597503e39073b5b3c522f7db933fbac202d91d24492693fd" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "memchr", ] @@ -1814,11 +1822,10 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -1829,7 +1836,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.1", + "event-listener 5.4.2", "pin-project-lite", ] @@ -2179,7 +2186,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.2", + "http 1.5.0", "indexmap 2.14.0", "slab", "tokio", @@ -2396,9 +2403,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2422,7 +2429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", - "http 1.4.2", + "http 1.5.0", ] [[package]] @@ -2433,7 +2440,7 @@ checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "pin-project-lite", ] @@ -2452,9 +2459,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] @@ -2493,7 +2500,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "httparse", "itoa", @@ -2509,10 +2516,10 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.2", + "http 1.5.0", "hyper 1.11.0", "hyper-util", - "rustls 0.23.42", + "rustls 0.23.43", "tokio", "tokio-rustls 0.26.4", "tower-service", @@ -2528,7 +2535,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "hyper 1.11.0", "ipnet", @@ -2720,9 +2727,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" dependencies = [ "serde", ] @@ -2761,9 +2768,9 @@ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" [[package]] name = "jiff" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ "defmt", "jiff-core", @@ -2789,9 +2796,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ "jiff-core", "proc-macro2", @@ -2865,9 +2872,9 @@ dependencies = [ [[package]] name = "job_scheduler_ng" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217723d58ee473953675d15f11e56898a611aca8ea044d5a34eabeade99ef613" +checksum = "576b4255ab9de8ce7b81060ec54b1b7f8499dfd6c16a66c4cd4cb1ad4eba27e3" dependencies = [ "chrono", "cron", @@ -2943,18 +2950,18 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.9", ] [[package]] name = "lettre" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" +checksum = "f2c646bd5cc763b1087b15493e29a64be6147ba8f19342004fa52048ee596eae" dependencies = [ "async-std", "async-trait", - "base64 0.22.1", + "base64 0.23.1", "email-encoding", "email_address", "fastrand", @@ -2967,7 +2974,7 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls 0.23.42", + "rustls 0.23.43", "rustls-native-certs", "serde", "socket2 0.6.5", @@ -3089,9 +3096,9 @@ dependencies = [ [[package]] name = "mea" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2640d335e7273dacdcf51044026139b2e269c3bb0dfc3f8cb3496b85e3f6a42c" +checksum = "31fc7d159de0085ab6dd7ff145a9819442cfd3d098f783263120503c3f3e58b0" dependencies = [ "slab", ] @@ -3176,7 +3183,7 @@ dependencies = [ "crossbeam-epoch", "crossbeam-utils", "equivalent", - "event-listener 5.4.1", + "event-listener 5.4.2", "futures-util", "parking_lot", "portable-atomic", @@ -3194,11 +3201,11 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.4.2", + "http 1.5.0", "httparse", "memchr", "mime", - "spin", + "spin 0.9.9", "tokio", "tokio-util", "version_check", @@ -3370,7 +3377,7 @@ dependencies = [ "base64 0.22.1", "chrono", "getrandom 0.2.17", - "http 1.4.2", + "http 1.5.0", "rand 0.8.7", "serde", "serde_json", @@ -3401,9 +3408,9 @@ dependencies = [ [[package]] name = "opendal" -version = "0.57.0" +version = "0.58.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c9c85ce253ff87225e7669979d877a20c98a06604ec9d6dd5f4473e08f1ae1" +checksum = "4f20562cc7447fcc915fc5c23df305a412ea80a733c9f2fd9e2d267e2815be6d" dependencies = [ "opendal-core", "opendal-service-fs", @@ -3412,24 +3419,22 @@ dependencies = [ [[package]] name = "opendal-core" -version = "0.57.0" +version = "0.58.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f8607c90e2c963a91467f50fb49fbc7fb3d573f88cea219ca59ccd3740b309" +checksum = "ec75551ff4cf3e57da98979f6a937aaa9ddb3915bf68cc17d03df733be6646ed" dependencies = [ "anyhow", - "base64 0.22.1", + "base64 0.23.1", "bytes", "futures", - "http 1.4.2", - "http-body 1.1.0", + "http 1.5.0", "jiff", "log", "md-5", "mea", "percent-encoding", - "quick-xml 0.39.4", + "quick-xml", "reqsign-core", - "reqwest", "serde", "serde_json", "tokio", @@ -3440,9 +3445,9 @@ dependencies = [ [[package]] name = "opendal-service-fs" -version = "0.57.0" +version = "0.58.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e89a665fef0e6bd249cf5ea47fc174b7ba892159bee4b9382528b1ca873a2c" +checksum = "826c4e17a30643b888fe983897f9a4b23b07066e1d069727a923cc8fb419a702" dependencies = [ "bytes", "log", @@ -3454,18 +3459,18 @@ dependencies = [ [[package]] name = "opendal-service-s3" -version = "0.57.0" +version = "0.58.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "313d46c9f5ae70bca26b7c3e3fbb9b639292625f28af73aa016f47e788af9deb" +checksum = "58e80cdf192d7eff05feed747894d64f81905ac4eaf132edf7ea270abdd2d663" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", - "crc32c", - "http 1.4.2", + "crc-fast", + "http 1.5.0", "log", "md-5", "opendal-core", - "quick-xml 0.39.4", + "quick-xml", "reqsign-aws-v4", "reqsign-core", "reqsign-file-read-tokio", @@ -3484,7 +3489,7 @@ dependencies = [ "dyn-clone", "ed25519-dalek", "hmac 0.12.1", - "http 1.4.2", + "http 1.5.0", "itertools", "log", "oauth2", @@ -4007,16 +4012,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.39.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quick-xml" version = "0.41.0" @@ -4193,9 +4188,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -4226,19 +4221,18 @@ dependencies = [ ] [[package]] -name = "reqsign-aws-v4" -version = "3.0.2" +name = "reqsign-aws-core" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9e1168fab3883ec6afed1c2e20c25b2a09f366cdb662ac3e0878ae0332d63e" +checksum = "e4af084e1f3cbf3e67e0c972765399bce54ecec804cceba46b39a8331f3c1bff" dependencies = [ - "anyhow", "bytes", "form_urlencoded", "hex", - "http 1.4.2", + "http 1.5.0", "log", "percent-encoding", - "quick-xml 0.41.0", + "quick-xml", "reqsign-core", "rust-ini", "serde", @@ -4248,19 +4242,33 @@ dependencies = [ ] [[package]] -name = "reqsign-core" +name = "reqsign-aws-v4" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514a1e0b4aa288652a3fdbda4f0a610f379cdf5374e55a37c9edd03d57ed856b" +checksum = "4ac5b3b7cefa28933792b439186459f77f19f9b6edbeab41b8b187150361a206" +dependencies = [ + "bytes", + "http 1.5.0", + "log", + "quick-xml", + "reqsign-aws-core", + "reqsign-core", + "serde", +] + +[[package]] +name = "reqsign-core" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07dd510b1e1b9b241883e483358147fb2ed2d497a7b39b065ba61eb93deceb0" dependencies = [ "anyhow", - "base64 0.22.1", + "base64 0.23.1", "bytes", - "form_urlencoded", "futures", "hex", "hmac 0.13.0", - "http 1.4.2", + "http 1.5.0", "jiff", "log", "percent-encoding", @@ -4271,9 +4279,9 @@ dependencies = [ [[package]] name = "reqsign-file-read-tokio" -version = "3.0.2" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b472a8d1f2e5a4be8ce13bb7bdf4b59e9bee613ce124aca23959ddb42176b39" +checksum = "663d9d55abd0df0830ef0ae43708297cc1371cf4e8ca91f3ac813c309cca8c98" dependencies = [ "anyhow", "reqsign-core", @@ -4291,11 +4299,10 @@ dependencies = [ "cookie", "cookie_store", "encoding_rs", - "futures-channel", "futures-core", "futures-util", "h2", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "hyper 1.11.0", @@ -4306,7 +4313,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustls 0.23.42", + "rustls 0.23.43", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -4575,9 +4582,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -4629,7 +4636,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.42", + "rustls 0.23.43", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.13", @@ -4719,9 +4726,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "ref-cast", @@ -4921,7 +4928,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.14.0", "schemars 0.9.0", - "schemars 1.2.1", + "schemars 1.2.2", "serde_core", "serde_json", "serde_with_macros", @@ -5107,6 +5114,12 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + [[package]] name = "spinning_top" version = "0.3.0" @@ -5330,20 +5343,11 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "libc", @@ -5424,13 +5428,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -5449,7 +5453,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.42", + "rustls 0.23.43", "tokio", ] @@ -5550,9 +5554,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow 1.0.4", ] @@ -5601,7 +5605,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "pin-project-lite", @@ -5702,7 +5706,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.4.2", + "http 1.5.0", "httparse", "log", "rand 0.8.7", @@ -5818,9 +5822,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef73bfbaf3216cb59c205d7176bee1194e0d84348979da31f4a71fefe3c2054e" +checksum = "068e763e8279de7ab94b6afebded2cb701678af094feb1c12ccb061b4783c1be" [[package]] name = "vaultwarden" @@ -5853,7 +5857,7 @@ dependencies = [ "handlebars", "hickory-resolver", "html5gum", - "http 1.4.2", + "http 1.5.0", "ipnet", "job_scheduler_ng", "jsonwebtoken", @@ -5881,7 +5885,7 @@ dependencies = [ "rocket", "rocket_ws", "rpassword", - "rustls 0.23.42", + "rustls 0.23.43", "semver", "serde", "serde_json", @@ -6392,15 +6396,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.6.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.7.15" @@ -6501,18 +6496,15 @@ dependencies = [ [[package]] name = "yubico_ng" -version = "0.15.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228e2862e3c66f3224102d9a00d9d3646b271a05cc6c4819fea195fa8b5c00e0" +checksum = "563eb0ab41031e758446e3737231541bb4556d6af1e893e94faa09c989f794af" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "form_urlencoded", - "futures", - "hmac 0.12.1", - "rand 0.9.5", - "reqwest", - "sha1 0.10.7", - "threadpool", + "getrandom 0.4.3", + "hmac 0.13.0", + "sha1 0.11.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8bc72255..4f2fb283 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ libsqlite3-sys = { version = "0.37.0", optional = true } # Crypto-related libraries rand = "0.10.2" ring = "0.17.14" -rustls = { version = "0.23.42", features = ["ring", "std"], default-features = false } +rustls = { version = "0.23.43", features = ["ring", "std"], default-features = false } subtle = "2.6.1" # UUID generation @@ -133,13 +133,13 @@ uuid = { version = "1.24.0", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.45", default-features = false, features = ["clock", "serde"] } chrono-tz = "0.10.4" -time = "0.3.54" +time = "0.3.55" # Job scheduler -job_scheduler_ng = "2.4.0" +job_scheduler_ng = "2.5.0" # Data encoding library Hex/Base32/Base64 -data-encoding = "2.11.0" +data-encoding = "2.11.1" # JWT library jsonwebtoken = { version = "11.0.0", default-features = false, features = ["rust_crypto", "use_pem"] } @@ -148,7 +148,7 @@ jsonwebtoken = { version = "11.0.0", default-features = false, features = ["rust totp-lite = "2.0.1" # Yubico Library -yubico = { package = "yubico_ng", version = "0.15.0", default-features = false, features = ["online-tokio"] } +yubico_ng = { version = "1.0.0", default-features = false } # WebAuthn libraries # danger-allow-state-serialisation is needed to save the state in the db @@ -161,7 +161,7 @@ webauthn-rs-core = "0.5.5" url = "2.5.8" # Email libraries -lettre = { version = "0.11.22", default-features = false, features = [ +lettre = { version = "0.11.23", default-features = false, features = [ # Misc "tracing", "serde", @@ -231,7 +231,7 @@ pastey = "0.2.3" governor = "0.10.4" # CIDR parsing for the trusted proxies of the client IP header -ipnet = "2.12.0" +ipnet = "2.12.1" # OIDC for SSO openidconnect = { version = "4.0.1", default-features = false } @@ -256,10 +256,10 @@ rpassword = "7.5.4" grass_compiler = { version = "0.13.4", default-features = false } # File are accessed through Apache OpenDAL -opendal = { version = "0.57.0", default-features = false, features = ["services-fs"] } +opendal = { version = "0.58.1", default-features = false, features = ["services-fs"] } # For retrieving AWS credentials, including temporary SSO credentials -aws-config = { version = "1.10.0", optional = true, default-features = false, features = [ +aws-config = { version = "1.10.1", optional = true, default-features = false, features = [ "behavior-version-latest", "credentials-process", "rt-tokio", @@ -267,9 +267,9 @@ aws-config = { version = "1.10.0", optional = true, default-features = false, fe ] } aws-credential-types = { version = "1.3.0", optional = true } aws-smithy-runtime-api = { version = "1.14.0", optional = true } -http = { version = "1.4.2", optional = true } -reqsign-aws-v4 = { version = "3.0.2", optional = true } -reqsign-core = { version = "3.1.0", optional = true } +http = { version = "1.5.0", optional = true } +reqsign-aws-v4 = { version = "3.1.0", optional = true } +reqsign-core = { version = "3.2.1", optional = true } # Strip debuginfo from the release builds # The debug symbols are to provide better panic traces diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 4a51a6b2..4c5e851b 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,6 +1,6 @@ --- -vault_version: "v2026.6.4" -vault_image_digest: "sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427" +vault_version: "v2026.7.0" +vault_image_digest: "sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index baa4c979..7045138d 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.6.4 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.6.4 -# [docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.7.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.7.0 +# [docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427 -# [docker.io/vaultwarden/web-vault:v2026.6.4] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c +# [docker.io/vaultwarden/web-vault:v2026.7.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 @@ -70,7 +70,7 @@ RUN echo "export CARGO_TARGET=${CARGO_BUILD_TARGET}" >> /env-cargo && \ # Output the current contents of the file cat /env-cargo -RUN source /env-cargo && \ +RUN . /env-cargo && \ rustup target add "${CARGO_TARGET}" # Copies over *only* your manifests and build files @@ -86,7 +86,7 @@ ARG DB=sqlite,mysql,postgresql,enable_mimalloc # Builds your dependencies and removes the # dummy project, except the target folder # This folder contains the compiled dependencies -RUN source /env-cargo && \ +RUN . /env-cargo && \ cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ find . -not -path "./target*" -delete @@ -97,13 +97,13 @@ COPY . . ARG VW_VERSION # Builds again, this time it will be the actual source files being build -RUN source /env-cargo && \ +RUN . /env-cargo && \ # Make sure that we actually build the project by updating the src/main.rs timestamp # Also do this for build.rs to ensure the version is rechecked touch build.rs src/main.rs && \ # Create a symlink to the binary target folder to easy copy the binary in the final stage cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ - if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \ + if [ "${CARGO_PROFILE}" = "dev" ] ; then \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ else \ ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \ @@ -126,6 +126,7 @@ RUN source /env-cargo && \ # To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*' # # We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742 +# hadolint ignore=DL3065 FROM --platform=$TARGETPLATFORM docker.io/library/alpine:3.24 ENV ROCKET_PROFILE="release" \ diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 7ebb07bd..9ab02568 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2026.6.4 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.6.4 -# [docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427] +# $ docker pull docker.io/vaultwarden/web-vault:v2026.7.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2026.7.0 +# [docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427 -# [docker.io/vaultwarden/web-vault:v2026.6.4] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c +# [docker.io/vaultwarden/web-vault:v2026.7.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:e7d3b31ec6a991a6bf447721ea341b4192ce5d3b920929211672fd4f3f891427 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:ba8bab66d4330ab9dbafa8f245bcbe99cf6ee3f2c8ce9b5fbb10e9c49658451c AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts @@ -37,6 +37,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:c64defb9ed5a91eacb37f ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.97.1-slim-trixie AS build +# hadolint ignore=DL3067 COPY --from=xx / / ARG TARGETARCH ARG TARGETVARIANT @@ -80,7 +81,7 @@ RUN mkdir -pv "${CARGO_HOME}" && \ RUN USER=root cargo new --bin /app WORKDIR /app -RUN source /env-cargo && \ +RUN . /env-cargo && \ rustup target add "${CARGO_TARGET}" # Copies over *only* your manifests and build files @@ -95,9 +96,14 @@ ARG DB=sqlite,mysql,postgresql # Builds your dependencies and removes the # dummy project, except the target folder # This folder contains the compiled dependencies -RUN source /env-cargo && \ - # Workaround for xx related build issues +RUN . /env-cargo && \ + # Configure xx-cargo for target pkg-config and Debian transitive library lookup # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 + # https://github.com/dani-garcia/vaultwarden/discussions/7522 + if xx-info is-cross; then \ + XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \ + export XX_RUSTFLAGS; \ + fi && \ PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \ find . -not -path "./target*" -delete @@ -108,15 +114,20 @@ COPY . . ARG VW_VERSION # Builds again, this time it will be the actual source files being build -RUN source /env-cargo && \ +RUN . /env-cargo && \ # Make sure that we actually build the project by updating the src/main.rs timestamp # Also do this for build.rs to ensure the version is rechecked touch build.rs src/main.rs && \ # Create a symlink to the binary target folder to easy copy the binary in the final stage - # Workaround for xx related build issues + # Configure xx-cargo for target pkg-config and Debian transitive library lookup # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 + # https://github.com/dani-garcia/vaultwarden/discussions/7522 + if xx-info is-cross; then \ + XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \ + export XX_RUSTFLAGS; \ + fi && \ PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" && \ - if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \ + if [ "${CARGO_PROFILE}" = "dev" ] ; then \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ else \ ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \ @@ -139,6 +150,7 @@ RUN source /env-cargo && \ # To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*' # # We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742 +# hadolint ignore=DL3065 FROM --platform=$TARGETPLATFORM docker.io/library/debian:trixie-slim ENV ROCKET_PROFILE="release" \ diff --git a/docker/Dockerfile.j2 b/docker/Dockerfile.j2 index 5e33d512..633d6955 100644 --- a/docker/Dockerfile.j2 +++ b/docker/Dockerfile.j2 @@ -28,8 +28,13 @@ # [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}] # {% macro xx_cargo_config() -%} -# Workaround for xx related build issues +# Configure xx-cargo for target pkg-config and Debian transitive library lookup # https://github.com/tonistiigi/xx/pull/108#issuecomment-3700635977 + # https://github.com/dani-garcia/vaultwarden/discussions/7522 + if xx-info is-cross; then \ + XX_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,/usr/lib/$(xx-info triple)"; \ + export XX_RUSTFLAGS; \ + fi && \ PKG_CONFIG="$(command -v "$(xx-info)-pkg-config")" xx-cargo build --features ${DB} --profile "${CARGO_PROFILE}" {%- endmacro %} FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault @@ -52,6 +57,7 @@ FROM --platform=$BUILDPLATFORM {{ build_stage_image[base].arch_image[arch] }} AS # hadolint ignore=DL3006 FROM --platform=$BUILDPLATFORM {{ build_stage_image[base].image }} AS build {% if base == "debian" %} +# hadolint ignore=DL3067 COPY --from=xx / / {% endif %} ARG TARGETARCH @@ -111,7 +117,7 @@ RUN echo "export CARGO_TARGET=${CARGO_BUILD_TARGET}" >> /env-cargo && \ cat /env-cargo {% endif %} -RUN source /env-cargo && \ +RUN . /env-cargo && \ rustup target add "${CARGO_TARGET}" # Copies over *only* your manifests and build files @@ -131,7 +137,7 @@ ARG DB=sqlite,mysql,postgresql,enable_mimalloc # Builds your dependencies and removes the # dummy project, except the target folder # This folder contains the compiled dependencies -RUN source /env-cargo && \ +RUN . /env-cargo && \ {% if base == "debian" %} {{ xx_cargo_config() }} && \ {% elif base == "alpine" %} @@ -146,7 +152,7 @@ COPY . . ARG VW_VERSION # Builds again, this time it will be the actual source files being build -RUN source /env-cargo && \ +RUN . /env-cargo && \ # Make sure that we actually build the project by updating the src/main.rs timestamp # Also do this for build.rs to ensure the version is rechecked touch build.rs src/main.rs && \ @@ -156,7 +162,7 @@ RUN source /env-cargo && \ {% elif base == "alpine" %} cargo build --features ${DB} --profile "${CARGO_PROFILE}" --target="${CARGO_TARGET}" && \ {% endif %} - if [[ "${CARGO_PROFILE}" == "dev" ]] ; then \ + if [ "${CARGO_PROFILE}" = "dev" ] ; then \ ln -vfsr "/app/target/${CARGO_TARGET}/debug" /app/target/final ; \ else \ ln -vfsr "/app/target/${CARGO_TARGET}/${CARGO_PROFILE}" /app/target/final ; \ @@ -179,6 +185,7 @@ RUN source /env-cargo && \ # To uninstall: docker run --privileged --rm tonistiigi/binfmt --uninstall 'qemu-*' # # We need to add `--platform` here, because of a podman bug: https://github.com/containers/buildah/issues/4742 +# hadolint ignore=DL3065 FROM --platform=$TARGETPLATFORM {{ runtime_stage_image[base] }} ENV ROCKET_PROFILE="release" \ diff --git a/playwright/.env.template b/playwright/.env.template index a6696aab..4ead281d 100644 --- a/playwright/.env.template +++ b/playwright/.env.template @@ -21,11 +21,19 @@ TEST_USER3=test3 TEST_USER3_PASSWORD=${TEST_USER3} TEST_USER3_MAIL=${TEST_USER3}@yopmail.com +TEST_USER4=test4 +TEST_USER4_PASSWORD=${TEST_USER4} +TEST_USER4_MAIL=${TEST_USER4}@yopmail.com + +TEST_USER5=test5 +TEST_USER5_PASSWORD=${TEST_USER5} +TEST_USER5_MAIL=${TEST_USER5}@yopmail.com + ################### # Keycloak Config # ################### -KEYCLOAK_ADMIN=admin -KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN} +KC_BOOTSTRAP_ADMIN_USERNAME=admin +KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_USERNAME} KC_HTTP_HOST=127.0.0.1 KC_HTTP_PORT=8080 @@ -39,8 +47,10 @@ DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM} ###################### ROCKET_ADDRESS=0.0.0.0 ROCKET_PORT=8000 -DOMAIN=http://localhost:${ROCKET_PORT} +ROCKET_TLS={certs="/data/ssl/localhost.crt",key="/data/ssl/localhost.key"} +DOMAIN=https://127.0.0.1:${ROCKET_PORT} LOG_LEVEL=info,oidcwarden::sso=debug +SSO_DEBUG_TOKENS=true I_REALLY_WANT_VOLATILE_STORAGE=true SSO_ENABLED=true diff --git a/playwright/README.md b/playwright/README.md index a27e6105..000725d7 100644 --- a/playwright/README.md +++ b/playwright/README.md @@ -1,8 +1,8 @@ # Integration tests This allows running integration tests using [Playwright](https://playwright.dev/). - -It uses its own `test.env` with different ports to not collide with a running dev instance. +\ +It usse its own [test.env](/test/scenarios/test.env) with different ports to not collide with a running dev instance. ## Install @@ -11,11 +11,11 @@ Databases (`Mariadb`, `Mysql` and `Postgres`) and `Playwright` will run in conta ### Running Playwright outside docker -It is possible to run `Playwright` outside of the container, this removes the need to rebuild the image for each change. -You will additionally need `nodejs` then run: +It's possible to run `Playwright` outside of the container, this remove the need to rebuild the image for each change. +You'll additionally need `nodejs` then run: ```bash -npm ci --ignore-scripts +npm ci --ignore-scripts --allow-git=none --allow-remote=none npx playwright install-deps npx playwright install firefox ``` @@ -65,7 +65,7 @@ DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Pl If you want you can keep the DB and Keycloak runnning (states are not impacted by the tests): ```bash -PW_KEEP_SERVICE_RUNNNING=true npx playwright test +PW_KEEP_SERVICE_RUNNING=true npx playwright test ``` ### Running specific tests @@ -77,7 +77,7 @@ DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Pl DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite login ``` -To run only a specifc test (It might fail if it has dependency): +To run only a specific test (It might fail if it has dependency): ```bash DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite -g "Account creation" @@ -92,7 +92,7 @@ This does not start the server, you will need to start it manually. ```bash DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden -npx playwright codegen "http://127.0.0.1:8003" +npx playwright codegen "https://127.0.0.1:8000" --ignore-https-errors ``` ## Override web-vault @@ -112,12 +112,11 @@ You can check the result running: DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env up Vaultwarden ``` -Then check `http://127.0.0.1:8003/admin/diagnostics` with `admin`. +Then check `https://127.0.0.1:8003/admin/diagnostics` with `admin`. # OpenID Connect test setup -Additionally this `docker-compose` template allows to run locally Vaultwarden, -[Keycloak](https://www.keycloak.org/) and [Maildev](https://github.com/timshel/maildev) to test OIDC. +Additionally this `docker-compose` template allow to run locally `Vaultwarden`, [Keycloak](https://www.keycloak.org/) and [Maildev](https://github.com/timshel/maildev) to test OIDC. ## Setup @@ -131,18 +130,17 @@ Then start the stack (the `profile` is required to run `Vaultwarden`) : ```bash > docker compose --profile vaultwarden --env-file .env up .... -keycloakSetup_1 | Logging into http://127.0.0.1:8080 as user admin of realm master +keycloakSetup_1 | Logging into https://127.0.0.1:8080 as user admin of realm master keycloakSetup_1 | Created new realm with id 'test' keycloakSetup_1 | 74af4933-e386-4e64-ba15-a7b61212c45e oidc_keycloakSetup_1 exited with code 0 ``` -Wait until `oidc_keycloakSetup_1 exited with code 0` which indicates the correct setup of the Keycloak realm, client and user -(It is normal for this container to stop once the configuration is done). +Wait until `oidc_keycloakSetup_1 exited with code 0` which indicate the correct setup of the Keycloak realm, client and user (It's normal for this container to stop once the configuration is done). Then you can access : -- `Vaultwarden` on http://0.0.0.0:8000 with the default user `test@yopmail.com/test`. +- `Vaultwarden` on https://0.0.0.0:8000 with the default user `test@yopmail.com/test`. - `Keycloak` on http://0.0.0.0:8080/admin/master/console/ with the default user `admin/admin` - `Maildev` on http://0.0.0.0:1080 @@ -171,7 +169,7 @@ docker compose --profile vaultwarden --env-file .env build VaultwardenPrebuild V All configuration for `keycloak` / `Vaultwarden` / `keycloak_setup.sh` can be found in [.env](.env.template). The content of the file will be loaded as environment variables in all containers. -- `keycloak` [configuration](https://www.keycloak.org/server/all-config) includes `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD` and any variable prefixed `KC_` ([more information](https://www.keycloak.org/server/configuration#_example_configuring_the_db_url_host_parameter)). +- `keycloak` [configuration](https://www.keycloak.org/server/all-config) include `KC_BOOTSTRAP_ADMIN_USERNAME` / `KC_BOOTSTRAP_ADMIN_PASSWORD` and any variable prefixed `KC_` ([more information](https://www.keycloak.org/server/configuration#_example_configuring_the_db_url_host_parameter)). - All `Vaultwarden` configuration can be set (EX: `SMTP_*`) ## Cleanup diff --git a/playwright/compose/keycloak/setup.sh b/playwright/compose/keycloak/setup.sh index a27caaff..f1d8a303 100755 --- a/playwright/compose/keycloak/setup.sh +++ b/playwright/compose/keycloak/setup.sh @@ -17,7 +17,7 @@ done set -e -kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD" --client admin-cli +kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KC_BOOTSTRAP_ADMIN_USERNAME" --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" --client admin-cli kcadm.sh create realms -s realm="$TEST_REALM" -s enabled=true -s "accessTokenLifespan=600" kcadm.sh create clients -r test -s "clientId=$SSO_CLIENT_ID" -s "secret=$SSO_CLIENT_SECRET" -s "redirectUris=[\"$DOMAIN/*\"]" -i @@ -39,6 +39,6 @@ kcadm.sh create realms -s realm="$DUMMY_REALM" -s enabled=true -s "accessTokenLi # THEN in another terminal: # docker exec -it keycloakSetup-dev /bin/bash # export PATH=$PATH:/opt/keycloak/bin -# kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD" --client admin-cli +# kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KC_BOOTSTRAP_ADMIN_USERNAME" --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" --client admin-cli # ENJOY # Doc: https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/admin-cli.html diff --git a/playwright/compose/playwright/Dockerfile b/playwright/compose/playwright/Dockerfile index 4dae1ae4..6b48c7dc 100644 --- a/playwright/compose/playwright/Dockerfile +++ b/playwright/compose/playwright/Dockerfile @@ -28,7 +28,7 @@ RUN mkdir /playwright WORKDIR /playwright COPY package.json package-lock.json . -RUN npm ci --ignore-scripts && npx playwright install-deps && npx playwright install firefox +RUN npm ci --ignore-scripts --allow-git=none --allow-remote=none && npx playwright install-deps && npx playwright install firefox COPY docker-compose.yml test.env ./ COPY compose ./compose diff --git a/playwright/compose/warden/Dockerfile b/playwright/compose/warden/Dockerfile index e472d207..9a369dab 100644 --- a/playwright/compose/warden/Dockerfile +++ b/playwright/compose/warden/Dockerfile @@ -35,6 +35,7 @@ WORKDIR / COPY --from=prebuilt /start.sh . COPY --from=prebuilt /vaultwarden . +COPY --from=build /data ./data COPY --from=build /web-vault ./web-vault ENTRYPOINT ["/start.sh"] diff --git a/playwright/compose/warden/build.sh b/playwright/compose/warden/build.sh index 37e9a25e..ee8b47fe 100755 --- a/playwright/compose/warden/build.sh +++ b/playwright/compose/warden/build.sh @@ -22,3 +22,14 @@ if [[ ! -z "$REPO_URL" ]] && [[ ! -z "$COMMIT_HASH" ]] ; then mv build /web-vault fi + +# Lower the KDF iterations default for faster tests. +sed -i 's/(6e5,2e6,6e5)/(1e5,2e6,1e5)/' /web-vault/app/main.*.js + +# Generate a self signed cert +mkdir -p /data/ssl; cd /data/ssl + +openssl req -x509 -out localhost.crt -keyout localhost.key \ + -newkey rsa:2048 -nodes -sha256 \ + -subj '/CN=localhost' -extensions EXT -config <( \ + printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") diff --git a/playwright/docker-compose.yml b/playwright/docker-compose.yml index f4402326..5dd04ff4 100644 --- a/playwright/docker-compose.yml +++ b/playwright/docker-compose.yml @@ -24,12 +24,15 @@ services: environment: - ADMIN_TOKEN - DATABASE_URL + - CLIENT_SUPPRESS_ONBOARDING + - EMAIL_2FA_AUTO_FALLBACK - I_REALLY_WANT_VOLATILE_STORAGE - LOG_LEVEL - LOGIN_RATELIMIT_MAX_BURST - SMTP_HOST - SMTP_FROM - SMTP_DEBUG + - SSO_AUTH_ONLY_NOT_SESSION - SSO_DEBUG_TOKENS - SSO_ENABLED - SSO_FRONTEND @@ -70,7 +73,7 @@ services: Mysql: profiles: ["playwright"] container_name: playwright_mysql - image: mysql:8.4.1 + image: mysql:9.7.0 env_file: test.env healthcheck: test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] @@ -82,7 +85,7 @@ services: Postgres: profiles: ["playwright"] container_name: playwright_postgres - image: postgres:16.3 + image: postgres:18.4 env_file: test.env healthcheck: test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] @@ -94,7 +97,7 @@ services: Maildev: profiles: ["vaultwarden", "maildev"] container_name: maildev - image: timshel/maildev:3.0.4 + image: timshel/maildev:3.2.19 ports: - ${SMTP_PORT}:1025 - 1080:1080 @@ -102,7 +105,7 @@ services: Keycloak: profiles: ["keycloak", "vaultwarden"] container_name: keycloak-${ENV:-dev} - image: quay.io/keycloak/keycloak:26.3.4 + image: quay.io/keycloak/keycloak:26.6.2 network_mode: "host" command: - start-dev @@ -112,12 +115,12 @@ services: profiles: ["keycloak", "vaultwarden"] container_name: keycloakSetup-${ENV:-dev} image: keycloak_setup-${ENV:-dev} + network_mode: "host" build: context: compose/keycloak dockerfile: Dockerfile args: - KEYCLOAK_VERSION: 26.3.4 - network_mode: "host" + KEYCLOAK_VERSION: 26.6.2 depends_on: - Keycloak restart: "no" diff --git a/playwright/global-setup.ts b/playwright/global-setup.ts index 89405f12..9959d247 100644 --- a/playwright/global-setup.ts +++ b/playwright/global-setup.ts @@ -1,4 +1,4 @@ -import { firefox, type FullConfig } from '@playwright/test'; +import { type FullConfig } from '@playwright/test'; import { execSync } from 'node:child_process'; import fs from 'fs'; diff --git a/playwright/global-utils.ts b/playwright/global-utils.ts index 224bb4b8..937de651 100644 --- a/playwright/global-utils.ts +++ b/playwright/global-utils.ts @@ -207,7 +207,7 @@ export async function startVault(browser: Browser, testInfo: TestInfo, env = {}, } export async function stopVault(force: boolean = false) { - if( force === false && process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) { + if( force === false && process.env.PW_KEEP_SERVICE_RUNNING === "true" ) { console.log(`Keep vaultwarden running on: ${process.env.DOMAIN}`); } else { console.log(`Vaultwarden stopping`); @@ -231,6 +231,7 @@ export async function checkNotification(page: Page, hasText: string) { } export async function cleanLanding(page: Page) { + await page.context().clearCookies(); await page.goto('/', { waitUntil: 'domcontentloaded' }); await expect(page.getByRole('button').nth(0)).toBeVisible(); @@ -248,15 +249,3 @@ export async function logout(test: Test, page: Page, user: { name: string }) { await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible(); }); } - -export async function ignoreExtension(page: Page) { - await page.waitForLoadState('domcontentloaded'); - - try { - await page.getByRole('button', { name: 'Add it later' }).click({timeout: 5_000}); - await page.getByRole('link', { name: 'Skip to web app' }).click(); - } catch (error) { - console.log('Extension setup not visible. Continuing'); - } - -} diff --git a/playwright/package-lock.json b/playwright/package-lock.json index 2f4cd0c1..57f5bcaf 100644 --- a/playwright/package-lock.json +++ b/playwright/package-lock.json @@ -9,41 +9,56 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "mysql2": "3.15.3", - "otpauth": "9.4.1", - "pg": "8.16.3" + "mysql2": "3.22.3", + "otpauth": "9.5.1", + "pg": "8.21.0" }, "devDependencies": { - "@playwright/test": "1.56.1", - "dotenv": "17.2.3", - "dotenv-expand": "12.0.3", - "maildev": "npm:@timshel_npm/maildev@3.2.5" + "@playwright/test": "1.60.0", + "dotenv": "17.4.2", + "dotenv-expand": "13.0.0", + "maildev": "npm:@timshel_npm/maildev@3.2.19" } }, "node_modules/@asamuzakjp/css-color": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", - "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "lru-cache": "^11.2.1" + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.3.tgz", - "integrity": "sha512-kiGFeY+Hxf5KbPpjRLf+ffWbkos1aGo8MBfd91oxS3O57RgU3XhZrt/6UzoVF9VMpWbC3v87SRc9jxGrc9qHtQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", "dev": true, "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.2" + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/nwsapi": { @@ -52,10 +67,22 @@ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", "dev": true }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -68,13 +95,13 @@ } ], "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", "dev": true, "funding": [ { @@ -87,17 +114,17 @@ } ], "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", "dev": true, "funding": [ { @@ -110,21 +137,21 @@ } ], "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, "funding": [ { @@ -137,16 +164,16 @@ } ], "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.15.tgz", - "integrity": "sha512-q0p6zkVq2lJnmzZVPR33doA51G7YOja+FBvRdp5ISIthL0MtFCgYHHhR563z9WFGxcOn0WfjSkPDJ5Qig3H3Sw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", + "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", "dev": true, "funding": [ { @@ -158,14 +185,19 @@ "url": "https://opencollective.com/csstools" } ], - "engines": { - "node": ">=18" + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, "funding": [ { @@ -178,27 +210,44 @@ } ], "engines": { - "node": ">=18" + "node": ">=20.19.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@playwright/test": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", - "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", "dev": true, "dependencies": { - "playwright": "1.56.1" + "playwright": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -246,12 +295,11 @@ } }, "node_modules/@types/node": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", - "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", - "dev": true, + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.12.0" } }, "node_modules/@types/trusted-types": { @@ -261,6 +309,38 @@ "dev": true, "optional": true }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz", + "integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==", + "dev": true, + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1" + } + }, + "node_modules/@zone-eu/mailsplit/node_modules/libmime": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", + "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", + "dev": true, + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.6.3", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -289,15 +369,6 @@ "integrity": "sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg==", "dev": true }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -312,15 +383,6 @@ "node": ">= 6.0.0" } }, - "node_modules/base32.js": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", - "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -340,29 +402,33 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "dev": true, "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/body-parser/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -376,6 +442,22 @@ } } }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -421,9 +503,9 @@ } }, "node_modules/commander": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", - "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "engines": { "node": ">=20" @@ -460,15 +542,16 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "dev": true, - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -499,9 +582,9 @@ } }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "dependencies": { "object-assign": "^4", @@ -509,46 +592,36 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/cssstyle": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", - "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", - "dev": true, - "dependencies": { - "@asamuzakjp/css-color": "^4.0.3", - "@csstools/css-syntax-patches-for-csstree": "^1.0.14", - "css-tree": "^3.1.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/data-urls": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", - "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=20" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/debug": { @@ -634,9 +707,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", - "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", + "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", "dev": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -657,9 +730,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "dev": true, "engines": { "node": ">=12" @@ -669,12 +742,12 @@ } }, "node_modules/dotenv-expand": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", - "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-13.0.0.tgz", + "integrity": "sha512-aBfBS8eYIeXmpHI9ThIlA7/WLq+SLt18iXUZhb52rW89QLKQFoIpPG1bPeewoPZsTyjSSO3T7234FBVUM1V2rA==", "dev": true, "dependencies": { - "dotenv": "^16.4.5" + "dotenv": "^17.4.2" }, "engines": { "node": ">=12" @@ -683,18 +756,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/dotenv-expand/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -734,20 +795,21 @@ } }, "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "version": "6.6.8", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz", + "integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==", "dev": true, "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "ws": "~8.20.1" }, "engines": { "node": ">=10.2.0" @@ -776,9 +838,9 @@ } }, "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -828,27 +890,6 @@ "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -907,18 +948,19 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -949,9 +991,9 @@ } }, "node_modules/express/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -972,9 +1014,9 @@ "dev": true }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "dependencies": { "debug": "^4.4.0", @@ -985,13 +1027,17 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/finalhandler/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -1122,9 +1168,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "dependencies": { "function-bind": "^1.1.2" @@ -1143,15 +1189,15 @@ } }, "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, "dependencies": { - "whatwg-encoding": "^3.1.1" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/html-to-text": { @@ -1190,102 +1236,25 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/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==", - "dev": true - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/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==", - "dev": true - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -1337,34 +1306,35 @@ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" }, "node_modules/jsdom": { - "version": "27.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz", - "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", + "version": "29.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.0.tgz", + "integrity": "sha512-YNUc7fB9QuvSSQWfrH0xF+TyABkxUwx8sswgIDaCrw4Hol8BghdZDkITtZheRJeMtzWlnTfsM3bBBusRvpO1wg==", "dev": true, "dependencies": { - "@asamuzakjp/dom-selector": "^6.7.2", - "cssstyle": "^5.3.1", - "data-urls": "^6.0.0", + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", + "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", - "rrweb-cssom": "^0.8.0", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.1.0", - "ws": "^8.18.3", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=20" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -1391,17 +1361,33 @@ "dev": true }, "node_modules/libmime": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", - "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.8.tgz", + "integrity": "sha512-ZrCY+Q66mPvasAfjsQ/IgahzoBvfE1VdtGRpo1hwRB1oK3wJKxhKA3GOcd2a6j7AH5eMFccxK9fBoCpRZTf8ng==", "dev": true, "dependencies": { "encoding-japanese": "2.2.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.2", "libbase64": "1.3.0", "libqp": "2.1.1" } }, + "node_modules/libmime/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/libqp": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz", @@ -1423,18 +1409,18 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" }, "node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", "dev": true, "engines": { "node": "20 || >=22" } }, "node_modules/lru.min": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", - "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", "engines": { "bun": ">=1.0.0", "deno": ">=1.30.0", @@ -1447,25 +1433,25 @@ }, "node_modules/maildev": { "name": "@timshel_npm/maildev", - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@timshel_npm/maildev/-/maildev-3.2.5.tgz", - "integrity": "sha512-suWQu2s2kmO+MXtNJYW9peklznhd+aorIUb4tSNrfaKoEJjDa3vLXTvWf+3cb67o4Yv4Z6nPeKdMTCDZVn/Nyw==", + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@timshel_npm/maildev/-/maildev-3.2.19.tgz", + "integrity": "sha512-A/f07Fe7hCFy/2cUo0xg2r349RvOAHi4TuqOlZXxPuWShWbiSbvYcWr5wDz7G8aV5RyggNP0m+yB/lwmYx5OQg==", "dev": true, "dependencies": { "@types/mailparser": "3.4.6", "addressparser": "1.0.1", "async": "3.2.6", - "commander": "14.0.1", + "commander": "14.0.3", "compression": "1.8.1", - "cors": "2.8.5", - "dompurify": "3.3.0", - "express": "5.1.0", - "jsdom": "27.0.1", - "mailparser": "3.7.5", + "cors": "2.8.6", + "dompurify": "3.4.1", + "express": "5.2.1", + "jsdom": "29.1.0", + "mailparser": "3.9.8", "mime": "4.1.0", - "nodemailer": "7.0.9", - "smtp-server": "3.15.0", - "socket.io": "4.8.1", + "nodemailer": "8.0.7", + "smtp-server": "3.18.4", + "socket.io": "4.8.3", "wildstring": "1.0.9" }, "bin": { @@ -1476,27 +1462,27 @@ } }, "node_modules/mailparser": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.5.tgz", - "integrity": "sha512-o59RgZC+4SyCOn4xRH1mtRiZ1PbEmi6si6Ufnd3tbX/V9zmZN1qcqu8xbXY62H6CwIclOT3ppm5u/wV2nujn4g==", + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.8.tgz", + "integrity": "sha512-7jSlFGXiianVnhnb6wdutJFloD34488nrHY7r6FNqwXAhZ7YiJDYrKKTxZJ0oSrXcAPHm8YoYnh97xyGtrBQ3w==", "dev": true, "dependencies": { + "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", "he": "1.2.0", "html-to-text": "9.0.5", - "iconv-lite": "0.7.0", - "libmime": "5.3.7", + "iconv-lite": "0.7.2", + "libmime": "5.3.8", "linkify-it": "5.0.0", - "mailsplit": "5.4.6", - "nodemailer": "7.0.9", + "nodemailer": "8.0.5", "punycode.js": "2.3.1", - "tlds": "1.260.0" + "tlds": "1.261.0" } }, "node_modules/mailparser/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -1509,16 +1495,13 @@ "url": "https://opencollective.com/express" } }, - "node_modules/mailsplit": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.6.tgz", - "integrity": "sha512-M+cqmzaPG/mEiCDmqQUz8L177JZLZmXAUpq38owtpq2xlXlTSw+kntnxRt2xsxVFFV6+T8Mj/U0l5s7s6e0rNw==", - "deprecated": "This package has been renamed to @zone-eu/mailsplit. Please update your dependencies.", + "node_modules/mailparser/node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", "dev": true, - "dependencies": { - "libbase64": "1.3.0", - "libmime": "5.3.7", - "libqp": "2.1.1" + "engines": { + "node": ">=6.0.0" } }, "node_modules/math-intrinsics": { @@ -1531,9 +1514,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true }, "node_modules/media-typer": { @@ -1582,15 +1565,19 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ms": { @@ -1600,28 +1587,30 @@ "dev": true }, "node_modules/mysql2": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", - "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.3.tgz", + "integrity": "sha512-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA==", "dependencies": { - "aws-ssl-profiles": "^1.1.1", + "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", - "iconv-lite": "^0.7.0", - "long": "^5.2.1", - "lru.min": "^1.0.0", - "named-placeholders": "^1.1.3", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" }, "engines": { "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" } }, "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -1634,22 +1623,14 @@ } }, "node_modules/named-placeholders": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", - "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", "dependencies": { - "lru-cache": "^7.14.1" + "lru.min": "^1.1.0" }, "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/named-placeholders/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "engines": { - "node": ">=12" + "node": ">=8.0.0" } }, "node_modules/negotiator": { @@ -1662,9 +1643,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", - "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz", + "integrity": "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==", "dev": true, "engines": { "node": ">=6.0.0" @@ -1722,35 +1703,35 @@ } }, "node_modules/otpauth": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.4.1.tgz", - "integrity": "sha512-+iVvys36CFsyXEqfNftQm1II7SW23W1wx9RwNk0Cd97lbvorqAhBDksb/0bYry087QMxjiuBS0wokdoZ0iUeAw==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.5.1.tgz", + "integrity": "sha512-fJmDAHc8wImfqqqOXIlBvT1dEKrZK0Cmb2VEgScpNTolCz0PHh6ExUZGv4sLtOsWNaHCQlD+rRqaPgnoxFoZjQ==", "dependencies": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "2.2.0" }, "funding": { "url": "https://github.com/hectorm/otpauth?sponsor=1" } }, "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "dependencies": { - "entities": "^6.0.0" + "entities": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -1779,12 +1760,13 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, - "engines": { - "node": ">=16" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/peberminta": { @@ -1797,13 +1779,13 @@ } }, "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, @@ -1811,7 +1793,7 @@ "node": ">= 16.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.2.7" + "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -1823,15 +1805,15 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==" + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==" }, "node_modules/pg-int8": { "version": "1.0.1", @@ -1842,17 +1824,17 @@ } }, "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==" + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==" }, "node_modules/pg-types": { "version": "2.2.0", @@ -1878,12 +1860,12 @@ } }, "node_modules/playwright": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", - "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", "dev": true, "dependencies": { - "playwright-core": "1.56.1" + "playwright-core": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -1896,9 +1878,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", - "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -1974,9 +1956,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "dependencies": { "side-channel": "^1.1.0" @@ -1998,18 +1980,34 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/require-from-string": { @@ -2038,9 +2036,9 @@ } }, "node_modules/router/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -2060,12 +2058,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2116,31 +2108,35 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/send/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -2160,15 +2156,10 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" - }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "dependencies": { "encodeurl": "^2.0.0", @@ -2178,6 +2169,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -2206,13 +2201,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -2259,30 +2254,38 @@ } }, "node_modules/smtp-server": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/smtp-server/-/smtp-server-3.15.0.tgz", - "integrity": "sha512-yv945vk0/xcukSKAoIhGz6GOlcXoCyGQH2w9IlLrTKk3SJiOBH9bcO6tD0ILTZYJsMqRa6OTRZAyqeuLXkv59Q==", + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/smtp-server/-/smtp-server-3.18.4.tgz", + "integrity": "sha512-9EnXPG4Tv+2P/TSEUdFTduYn9IxtxNRsOq/ryVj8ZlT+6MU2um9gn2Td2hHlgH1n+saagMWtici3hn5J5PhU+g==", "dev": true, "dependencies": { - "base32.js": "0.1.0", "ipv6-normalize": "1.0.1", - "nodemailer": "7.0.9", + "nodemailer": "8.0.5", "punycode.js": "2.3.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.18.0" + } + }, + "node_modules/smtp-server/node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "dev": true, + "engines": { + "node": ">=6.0.0" } }, "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", "dev": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", - "debug": "~4.3.2", + "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" @@ -2292,19 +2295,19 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.7.tgz", + "integrity": "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==", "dev": true, "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" + "debug": "~4.4.1", + "ws": "~8.20.1" } }, "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -2324,44 +2327,23 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", "dev": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "debug": "~4.4.1" }, "engines": { "node": ">=10.0.0" } }, "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -2395,9 +2377,9 @@ } }, "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -2464,12 +2446,18 @@ "node": ">= 10.x" } }, - "node_modules/sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", "engines": { - "node": ">= 0.6" + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" } }, "node_modules/statuses": { @@ -2488,30 +2476,30 @@ "dev": true }, "node_modules/tlds": { - "version": "1.260.0", - "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.260.0.tgz", - "integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==", + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", "dev": true, "bin": { "tlds": "bin.js" } }, "node_modules/tldts": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", - "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", "dev": true, "dependencies": { - "tldts-core": "^7.0.17" + "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", - "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", "dev": true }, "node_modules/toidentifier": { @@ -2524,9 +2512,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "dependencies": { "tldts": "^7.0.5" @@ -2548,17 +2536,34 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dev": true, "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/uc.micro": { @@ -2567,11 +2572,19 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==" }, "node_modules/unpipe": { "version": "1.0.0", @@ -2604,46 +2617,35 @@ } }, "node_modules/webidl-conversions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", - "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, "engines": { "node": ">=20" } }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/whatwg-url": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", - "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "dependencies": { + "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", - "webidl-conversions": "^8.0.0" + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">=20" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/wildstring": { @@ -2659,9 +2661,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/playwright/package.json b/playwright/package.json index f47ec5dc..a7a35734 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -8,14 +8,14 @@ "author": "", "license": "ISC", "devDependencies": { - "@playwright/test": "1.56.1", - "dotenv": "17.2.3", - "dotenv-expand": "12.0.3", - "maildev": "npm:@timshel_npm/maildev@3.2.5" + "@playwright/test": "1.60.0", + "dotenv": "17.4.2", + "dotenv-expand": "13.0.0", + "maildev": "npm:@timshel_npm/maildev@3.2.19" }, "dependencies": { - "mysql2": "3.15.3", - "otpauth": "9.4.1", - "pg": "8.16.3" + "mysql2": "3.22.3", + "otpauth": "9.5.1", + "pg": "8.21.0" } } diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index de721aa3..ba5885d9 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -25,10 +25,12 @@ export default defineConfig({ /* Long global timeout for complex tests * But short action/nav/expect timeouts to fail on specific step (raise locally if not enough). */ - timeout: 120 * 1000, - actionTimeout: 20 * 1000, - navigationTimeout: 20 * 1000, - expect: { timeout: 20 * 1000 }, + timeout: 240 * 1000, + actionTimeout: 40 * 1000, + navigationTimeout: 40 * 1000, + expect: { timeout: 40 * 1000 }, + + "permissions": ["clipboard-read"], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { @@ -37,6 +39,10 @@ export default defineConfig({ browserName: 'firefox', locale: 'en-GB', timezoneId: 'Europe/London', + ignoreHTTPSErrors: true, + launchOptions: { + args: ['--ignore-certificate-errors'] + }, /* Always collect trace (other values add random test failures) See https://playwright.dev/docs/trace-viewer */ trace: 'on', diff --git a/playwright/test.env b/playwright/test.env index df182ebe..2260f860 100644 --- a/playwright/test.env +++ b/playwright/test.env @@ -10,7 +10,7 @@ DOCKER_BUILDKIT=1 ##################### # Playwright Config # ##################### -PW_KEEP_SERVICE_RUNNNING=${PW_KEEP_SERVICE_RUNNNING:-false} +PW_KEEP_SERVICE_RUNNING=${PW_KEEP_SERVICE_RUNNING:-false} PW_SMTP_FROM=vaultwarden@playwright.test ##################### @@ -38,8 +38,8 @@ TEST_USER3_MAIL=${TEST_USER3}@example.com ################### # Keycloak Config # ################### -KEYCLOAK_ADMIN=admin -KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN} +KC_BOOTSTRAP_ADMIN_USERNAME=admin +KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_USERNAME} KC_HTTP_HOST=127.0.0.1 KC_HTTP_PORT=8081 @@ -52,10 +52,12 @@ DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM} # Vaultwarden Config # ###################### ROCKET_PORT=8003 -DOMAIN=http://localhost:${ROCKET_PORT} +ROCKET_TLS={certs="/data/ssl/localhost.crt",key="/data/ssl/localhost.key"} +DOMAIN=https://127.0.0.1:${ROCKET_PORT} LOG_LEVEL=info,oidcwarden::sso=debug LOGIN_RATELIMIT_MAX_BURST=100 ADMIN_TOKEN=admin +CLIENT_SUPPRESS_ONBOARDING=true SMTP_SECURITY=off SMTP_PORT=${MAILDEV_SMTP_PORT} diff --git a/playwright/tests/collection.spec.ts b/playwright/tests/collection.spec.ts index 786a4644..867386a5 100644 --- a/playwright/tests/collection.spec.ts +++ b/playwright/tests/collection.spec.ts @@ -1,6 +1,8 @@ import { test, expect, type TestInfo } from '@playwright/test'; import * as utils from "../global-utils"; + +import * as orgs from './setups/orgs'; import { createAccount } from './setups/user'; let users = utils.loadEnv(); @@ -16,20 +18,12 @@ test.afterAll('Teardown', async ({}) => { test('Create', async ({ page }) => { await createAccount(test, page, users.user1); - await test.step('Create Org', async () => { - await page.getByRole('link', { name: 'New organisation' }).click(); - await page.getByLabel('Organisation name (required)').fill('Test'); - await page.getByRole('button', { name: 'Submit' }).click(); - await page.locator('div').filter({ hasText: 'Members' }).nth(2).click(); - - await utils.checkNotification(page, 'Organisation created'); - }); + await orgs.create(test, page, 'New organisation'); await test.step('Create Collection', async () => { - await page.getByRole('link', { name: 'Collections' }).click(); - await page.getByRole('button', { name: 'New' }).click(); + await page.getByRole('button', { name: 'New', exact: true }).click(); await page.getByRole('menuitem', { name: 'Collection' }).click(); - await page.getByLabel('Name (required)').fill('RandomCollec'); + await page.getByRole('textbox', { name: 'Name * (required)', exact: true }).fill('RandomCollec'); await page.getByRole('button', { name: 'Save' }).click(); await utils.checkNotification(page, 'Created collection RandomCollec'); await expect(page.getByRole('button', { name: 'RandomCollec' })).toBeVisible(); diff --git a/playwright/tests/cyphers.spec.ts b/playwright/tests/cyphers.spec.ts new file mode 100644 index 00000000..679874de --- /dev/null +++ b/playwright/tests/cyphers.spec.ts @@ -0,0 +1,56 @@ +import { test, expect, type Page, type TestInfo } from '@playwright/test'; +import * as OTPAuth from "otpauth"; + +import * as utils from "../global-utils"; +import { createAccount, logUser } from './setups/user'; +import { activateTOTP, disableTOTP } from './setups/2fa'; + +let users = utils.loadEnv(); +let totp; + +test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { + await utils.startVault(browser, testInfo, {}); +}); + +test.afterAll('Teardown', async ({}) => { + utils.stopVault(); +}); + +test('Change Key settings', async ({ page }) => { + await createAccount(test, page, users.user1); + + await test.step('Change SHA-256 Iterations', async () => { + await page.getByRole('button', { name: 'Toggle collapse Settings' }).click(); + await page.getByRole('link', { name: 'Security' }).click(); + await page.getByRole('link', { name: 'Keys' }).click(); + + await page.getByRole('spinbutton', { name: 'KDF iterations * (required)'}).fill('700000'); + + await page.getByRole('button', { name: 'Update encryption settings' }).click(); + await page.getByRole('textbox', { name: 'Master password * (required)' }).fill(users.user1.password); + await page.getByRole('button', { name: 'Update settings' }).click(); + await page.getByRole('heading', { name: 'Log in' }).click(); + }); + + await logUser(test, page, users.user1); + + await test.step('Switch to Argon2', async () => { + await page.getByRole('button', { name: 'Toggle collapse Settings' }).click(); + await page.getByRole('link', { name: 'Security' }).click(); + await page.getByRole('link', { name: 'Keys' }).click(); + + await page.locator('.ng-arrow-wrapper').click(); + await page.getByText('Argon2id').click(); + + await page.getByRole('spinbutton', { name: 'KDF memory (MB) * (required)'}).fill('16'); + await page.getByRole('spinbutton', { name: 'KDF iterations * (required)'}).fill('2'); + await page.getByRole('spinbutton', { name: 'KDF parallelism * (required)'}).fill('1'); + + await page.getByRole('button', { name: 'Update encryption settings' }).click(); + await page.getByRole('textbox', { name: 'Master password * (required)' }).fill(users.user1.password); + await page.getByRole('button', { name: 'Update settings' }).click(); + await page.getByRole('heading', { name: 'Log in' }).click(); + }); + + await logUser(test, page, users.user1); +}); diff --git a/playwright/tests/login.smtp.spec.ts b/playwright/tests/login.smtp.spec.ts index 87474b79..c5c4d9ba 100644 --- a/playwright/tests/login.smtp.spec.ts +++ b/playwright/tests/login.smtp.spec.ts @@ -41,13 +41,10 @@ test('Account creation', async ({ page }) => { test('Login', async ({ context, page }) => { const mailBuffer = mailserver.buffer(users.user1.email); - await logUser(test, page, users.user1, mailBuffer); + await logUser(test, page, users.user1, { mailBuffer }); await test.step('verify email', async () => { - await page.getByText('Verify your account\'s email').click(); - await expect(page.getByText('Verify your account\'s email')).toBeVisible(); - await page.getByRole('button', { name: 'Send email' }).click(); - + await page.getByRole('button', { name: "Send email" }).click(); await utils.checkNotification(page, 'Check your email inbox for a verification link'); const verify = await mailBuffer.expect((m) => m.subject === "Verify Your Email"); @@ -78,26 +75,10 @@ test('Activate 2fa', async ({ page }) => { test('2fa', async ({ page }) => { const emails = mailserver.buffer(users.user1.email); - await test.step('login', async () => { - await page.goto('/'); - - await page.getByLabel(/Email address/).fill(users.user1.email); - await page.getByRole('button', { name: 'Continue' }).click(); - await page.getByLabel('Master password').fill(users.user1.password); - await page.getByRole('button', { name: 'Log in with master password' }).click(); - - await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible(); - const code = await retrieveEmailCode(test, page, emails); - await page.getByLabel(/Verification code/).fill(code); - await page.getByRole('button', { name: 'Continue' }).click(); - - await page.getByRole('button', { name: 'Add it later' }).click(); - await page.getByRole('link', { name: 'Skip to web app' }).click(); - - await expect(page).toHaveTitle(/Vaults/); - }) - - await disableEmail(test, page, users.user1); + await logUser(test, page, users.user1, { + mailBuffer: emails, + mail2fa: true, + }); emails.close(); }); diff --git a/playwright/tests/login.spec.ts b/playwright/tests/login.spec.ts index aaac4708..194976ea 100644 --- a/playwright/tests/login.spec.ts +++ b/playwright/tests/login.spec.ts @@ -37,8 +37,8 @@ test('Authenticator 2fa', async ({ page }) => { await page.getByLabel(/Email address/).fill(users.user1.email); await page.getByRole('button', { name: 'Continue' }).click(); - await page.getByLabel('Master password').fill(users.user1.password); - await page.getByRole('button', { name: 'Log in with master password' }).click(); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user1.password); + await page.getByRole('button', { name: 'Log in', exact: true }).click(); await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible(); await page.getByLabel(/Verification code/).fill(totp.generate({timestamp})); diff --git a/playwright/tests/organization.smtp.spec.ts b/playwright/tests/organization.smtp.spec.ts index 2be5fec1..6d0eb859 100644 --- a/playwright/tests/organization.smtp.spec.ts +++ b/playwright/tests/organization.smtp.spec.ts @@ -4,6 +4,7 @@ import { MailDev } from 'maildev'; import * as utils from '../global-utils'; import * as orgs from './setups/orgs'; import { createAccount, logUser } from './setups/user'; +import { activateTOTP } from './setups/2fa'; let users = utils.loadEnv(); @@ -20,6 +21,7 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { await utils.startVault(browser, testInfo, { SMTP_HOST: process.env.MAILDEV_HOST, SMTP_FROM: process.env.PW_SMTP_FROM, + EMAIL_2FA_AUTO_FALLBACK: "true", }); mail1Buffer = mailServer.buffer(users.user1.email); @@ -45,7 +47,7 @@ test('Invite users', async ({ page }) => { await orgs.policies(test, page, 'Test'); await page.getByRole('button', { name: 'Account recovery' }).click(); await page.getByRole('checkbox', { name: 'Turn on' }).check(); - await page.getByRole('checkbox', { name: 'Require new members' }).check(); + await page.getByRole('checkbox', { name: 'Automatically enroll new' }).check(); await page.getByRole('button', { name: 'Save' }).click(); await utils.checkNotification(page, 'Edited policy Account recovery'); }); @@ -66,18 +68,16 @@ test('invited with new account', async ({ page }) => { await page.goto(link); await expect(page).toHaveTitle(/Create account | Vaultwarden Web/); - //await page.getByLabel('Name').fill(users.user2.name); - await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password); - await page.getByLabel('Confirm master password (').fill(users.user2.password); + // await page.getByLabel('Name').fill(users.user2.name); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password); + await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password); await page.getByRole('button', { name: 'Create account' }).click(); await utils.checkNotification(page, 'Your new account has been created'); - await utils.checkNotification(page, 'Invitation accepted'); - await utils.ignoreExtension(page); - // Redirected to the vault await expect(page).toHaveTitle('Vaults | Vaultwarden Web'); // await utils.checkNotification(page, 'You have been logged in!'); + await utils.checkNotification(page, 'Successfully accepted your invitation'); }); await test.step('Check mails', async () => { @@ -100,21 +100,19 @@ test('invited with existing account', async ({ page }) => { await page.getByRole('button', { name: 'Continue' }).click(); // Unlock page - await page.getByLabel('Master password').fill(users.user3.password); - await page.getByRole('button', { name: 'Log in with master password' }).click(); - - await utils.checkNotification(page, 'Invitation accepted'); - await utils.ignoreExtension(page); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user3.password); + await page.getByRole('button', { name: 'Log in', exact: true }).click(); // We are now in the default vault page await expect(page).toHaveTitle(/Vaultwarden Web/); + await utils.checkNotification(page, 'Successfully accepted your invitation'); await mail3Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox'); await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted')); }); test('Confirm invited user', async ({ page }) => { - await logUser(test, page, users.user1, mail1Buffer); + await logUser(test, page, users.user1, { mailBuffer: mail1Buffer }); await orgs.members(test, page, 'Test'); await orgs.confirm(test, page, 'Test', users.user2.email); @@ -123,25 +121,26 @@ test('Confirm invited user', async ({ page }) => { }); test('Organization is visible', async ({ page }) => { - await logUser(test, page, users.user2, mail2Buffer); + await logUser(test, page, users.user2, { mailBuffer: mail2Buffer }); await page.getByRole('button', { name: 'vault: Test', exact: true }).click(); await expect(page.getByLabel('Filter: Default collection')).toBeVisible(); }); test('Recover user password', async ({ page }) => { - await logUser(test, page, users.user1, mail1Buffer); + await logUser(test, page, users.user1, { mailBuffer: mail1Buffer }); let newPassword = "TotoNewPassword"; await orgs.members(test, page, 'Test'); - await test.step(`Rrcover ${users.user2.email}`, async () => { + await test.step(`Recover ${users.user2.email}`, async () => { await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible(); await page.getByRole('row').filter({hasText: users.user2.email}).getByLabel('Options').click(); await page.getByRole('menuitem', { name: 'Recover account' }).click(); - await page.getByRole('textbox', { name: 'New master password (required)', exact: true }).fill(newPassword); - await page.getByRole('textbox', { name: 'Confirm new master password (' }).fill(newPassword); - await page.getByRole('button', { name: 'Save' }).click(); - await utils.checkNotification(page, 'Password reset success'); + await page.getByRole('textbox', { name: 'New master password * (required)', exact: true }).fill(newPassword); + await page.getByRole('textbox', { name: 'Confirm new master password * (' }).fill(newPassword); + await page.getByRole('button', { name: 'Save' }).click(); + await utils.checkNotification(page, 'Account recovery success'); + await mail2Buffer.expect((m) => m.subject.includes('Master Password Has Been Changed')); }); let user2 = { @@ -149,5 +148,8 @@ test('Recover user password', async ({ page }) => { name: users.user2.name, password: newPassword, }; - await logUser(test, page, user2, mail2Buffer); + await logUser(test, page, user2, { + mailBuffer: mail2Buffer, + notNewDevice: true, + }); }); diff --git a/playwright/tests/secrets.spec.ts b/playwright/tests/secrets.spec.ts new file mode 100644 index 00000000..e229d400 --- /dev/null +++ b/playwright/tests/secrets.spec.ts @@ -0,0 +1,110 @@ +import { test, expect, type Page, type TestInfo } from '@playwright/test'; +import * as OTPAuth from "otpauth"; + +import * as utils from "../global-utils"; +import { createAccount, logUser } from './setups/user'; + +let users = utils.loadEnv(); +let totp; + +test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { + await utils.startVault(browser, testInfo, {}); + + const context = await browser.newContext(); + const page = await context.newPage(); + await createAccount(test, page, users.user1); + await context.close(); +}); + +test.afterAll('Teardown', async ({}) => { + utils.stopVault(); +}); + +test('Password', async ({ context, page }, testInfo: TestInfo) => { + const label = 'Test Password'; + + await logUser(test, page, users.user1); + + await test.step('Create password entry', async () => { + await page.getByRole('button', { name: 'New item' }).click(); + await page.getByRole('textbox', { name: 'Item name * (required)' }).fill(label); + await page.getByRole('textbox', { name: 'Username' }).fill(users.user1.name); + await page.getByRole('textbox', { name: 'Password' }).fill(users.user1.password); + await page.getByRole('button', { name: 'Save' }).click(); + await utils.checkNotification(page, 'Item added'); + await page.getByRole('button', { name: 'Close' }).click(); + }); + + // Log again + await logUser(test, page, users.user1); + + await test.step('Check', async () => { + await page.getByRole('row').filter({ hasText: label }).getByRole('button', { name: label }).click(); + await page.getByTestId('copy-username').click(); + await utils.checkNotification(page, 'Username copied'); + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(users.user1.name) + await page.getByTestId('copy-password').click(); + await utils.checkNotification(page, 'Password copied'); + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(users.user1.password) + await page.getByRole('button', { name: 'Close' }).click(); + }); + + await test.step('Delete', async () => { + await page.getByRole('row').filter({ hasText: label }).getByLabel('Options').click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Yes' }).click(); + await utils.checkNotification(page, 'Item sent to bin'); + }); + + // Log again + await logUser(test, page, users.user1); + + await test.step('Deleted', async () => { + await expect(page.getByRole('row').filter({ hasText: label })).toHaveCount(0) + }); +}); + + +test('SSH Key', async ({ context, page }, testInfo: TestInfo) => { + const label = 'Test SSH key'; + + await logUser(test, page, users.user1); + + const privateKey = await test.step('Create key entry', async () => { + await page.getByRole('button', { name: 'New', exact: true }).click(); + await page.getByRole('menuitem', { name: 'SSH key' }).click(); + await page.getByRole('textbox', { name: 'Item name * (required)' }).fill('Test SSH key'); + await page.getByRole('button', { name: 'Save' }).click(); + await utils.checkNotification(page, 'Item added'); + + await page.getByRole('button', { name: 'Copy private key' }).click(); + await utils.checkNotification(page, 'Private key copied'); + return await page.evaluate(() => navigator.clipboard.readText()); + }); + + // Log again + await logUser(test, page, users.user1); + + await test.step('Check', async () => { + await page.getByRole('row').filter({ hasText: label }).getByRole('button', { name: label }).click(); + + await page.getByRole('button', { name: 'Copy private key' }).click(); + await utils.checkNotification(page, 'Private key copied'); + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(privateKey) + await page.getByRole('button', { name: 'Close' }).click(); + }); + + await test.step('Delete', async () => { + await page.getByRole('row').filter({ hasText: label }).getByLabel('Options').click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Yes' }).click(); + await utils.checkNotification(page, 'Item sent to bin'); + }); + + // Log again + await logUser(test, page, users.user1); + + await test.step('Deleted', async () => { + await expect(page.getByRole('row').filter({ hasText: label })).toHaveCount(0) + }) +}); diff --git a/playwright/tests/send.spec.ts b/playwright/tests/send.spec.ts index d27c3ffc..ddb8009d 100644 --- a/playwright/tests/send.spec.ts +++ b/playwright/tests/send.spec.ts @@ -21,11 +21,11 @@ test('Send', async ({ browser, page }) => { await page.getByRole('link', { name: 'Send' }).click(); await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible(); - await page.getByRole('button', { name: 'New', exact: true }).click(); + await page.getByRole('button', { name: 'New Send', exact: true }).click(); await page.getByRole('menuitem', { name: 'Text' }).click(); - await page.getByRole('textbox', { name: 'Send name (required)' }).fill('Test'); - await page.getByRole('textbox', { name: 'Text to share (required)' }).fill('test'); + await page.getByRole('textbox', { name: 'Send name * (required)' }).fill('Test'); + await page.getByRole('textbox', { name: 'Text to share * (required)' }).fill('test'); await page.getByRole('button', { name: 'Save' }).click(); await page.locator('footer').getByRole('button', { name: 'Copy link' }).click(); @@ -46,14 +46,14 @@ test('Send', async ({ browser, page }) => { await page.getByRole('link', { name: 'Send' }).click(); await expect(page.locator('#main-content').getByText('Send', { exact: true })).toBeVisible(); - await page.getByRole('button', { name: 'New', exact: true }).click(); + await page.getByRole('button', { name: 'New' }).click(); await page.getByRole('menuitem', { name: 'Text' }).click(); - await page.getByRole('textbox', { name: 'Send name (required)' }).fill('Password'); - await page.getByRole('textbox', { name: 'Text to share (required)' }).fill('password'); + await page.getByRole('textbox', { name: 'Send name * (required)' }).fill('Password'); + await page.getByRole('textbox', { name: 'Text to share * (required)' }).fill('password'); await page.getByRole('combobox', { name: 'Who can view' }).click(); await page.getByText('Anyone with a password set by you').click(); - await page.getByRole('textbox', { name: 'Password (required)' }).fill('password'); + await page.getByRole('textbox', { name: 'Password * (required)', exact: true }).fill('password'); await page.getByRole('button', { name: 'Save' }).click(); await page.locator('footer').getByRole('button', { name: 'Copy link' }).click(); @@ -64,7 +64,7 @@ test('Send', async ({ browser, page }) => { await test.step('View with password', async () => { await page2.goto(pwd_url, { waitUntil: 'domcontentloaded' }); await expect(page2.getByRole('heading', { name: 'Enter the password to view' })).toBeVisible(); - await page2.getByRole('textbox', { name: 'Password (required)' }).fill('password'); + await page2.getByRole('textbox', { name: 'Password * (required)' }).fill('password'); await page2.getByRole('button', { name: 'Continue' }).click(); await expect(page2.getByRole('heading', { name: 'View Send' })).toBeVisible(); await expect(await page2.getByRole('paragraph').filter({ hasText: 'Password' })).toBeVisible(); diff --git a/playwright/tests/setups/2fa.ts b/playwright/tests/setups/2fa.ts index d7936420..d430d053 100644 --- a/playwright/tests/setups/2fa.ts +++ b/playwright/tests/setups/2fa.ts @@ -11,10 +11,11 @@ export async function activateTOTP(test: Test, page: Page, user: { name: string, await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click(); await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click(); - await page.getByLabel('Master password (required)').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByRole('button', { name: 'Continue' }).click(); - const secret = await page.getByLabel('Key').innerText(); + const secret = await page.getByLabel('Key', { exact: true }).innerText(); + let totp = new OTPAuth.TOTP({ secret, period: 30 }); await page.getByLabel(/Verification code/).fill(totp.generate()); @@ -33,8 +34,8 @@ export async function disableTOTP(test: Test, page: Page, user: { password: stri await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click(); await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click(); - await page.getByLabel('Master password (required)').click(); - await page.getByLabel('Master password (required)').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).click() + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Turn off' }).click(); await page.getByRole('button', { name: 'Yes' }).click(); @@ -49,7 +50,7 @@ export async function activateEmail(test: Test, page: Page, user: { name: string await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click(); await page.locator('bit-item').filter({ hasText: 'Enter a code sent to your email' }).getByRole('button').click(); - await page.getByLabel('Master password (required)').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Send email' }).click(); }); @@ -81,8 +82,8 @@ export async function disableEmail(test: Test, page: Page, user: { password: str await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click(); await page.locator('bit-item').filter({ hasText: 'Email' }).getByRole('button').click(); - await page.getByLabel('Master password (required)').click(); - await page.getByLabel('Master password (required)').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).click() + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Turn off' }).click(); await page.getByRole('button', { name: 'Yes' }).click(); diff --git a/playwright/tests/setups/admin.ts b/playwright/tests/setups/admin.ts new file mode 100644 index 00000000..354c9ee7 --- /dev/null +++ b/playwright/tests/setups/admin.ts @@ -0,0 +1,21 @@ +import { expect, type Browser, Page } from '@playwright/test'; +import * as utils from '../../global-utils'; + +utils.loadEnv(); + +export async function login(test, page: Page) { + await test.step(`Admin login`, async () => { + await page.goto('/admin'); + await page.getByRole('textbox', { name: 'Enter admin token' }).fill(process.env.ADMIN_TOKEN); + await page.getByRole('button', { name: 'Enter' }).click(); + }); +} + +export async function invite(test, page: Page, email: string) { + await test.step(`Invite user with ${email}`, async () => { + await page.getByRole('link', { name: 'Users' }).click(); + await page.getByRole('textbox', { name: 'Enter email' }).fill(email); + await page.getByRole('button', { name: 'Invite' }).click(); + await expect(page.getByRole('row', { name: email })).toHaveText(/Invited/); + }); +} diff --git a/playwright/tests/setups/db-teardown.ts b/playwright/tests/setups/db-teardown.ts index 5f753a9d..86d40ac5 100644 --- a/playwright/tests/setups/db-teardown.ts +++ b/playwright/tests/setups/db-teardown.ts @@ -5,7 +5,7 @@ const utils = require('../../global-utils'); utils.loadEnv(); test('DB teardown ?', async ({ serviceName }) => { - if( process.env.PW_KEEP_SERVICE_RUNNNING !== "true" ) { + if( process.env.PW_KEEP_SERVICE_RUNNING !== "true" ) { utils.stopComposeService(serviceName); } }); diff --git a/playwright/tests/setups/orgs.ts b/playwright/tests/setups/orgs.ts index 04d81b45..ce12c50e 100644 --- a/playwright/tests/setups/orgs.ts +++ b/playwright/tests/setups/orgs.ts @@ -3,11 +3,14 @@ import { expect, type Browser,Page } from '@playwright/test'; import * as utils from '../../global-utils'; export async function create(test, page: Page, name: string) { - await test.step('Create Org', async () => { - await page.locator('a').filter({ hasText: 'Password Manager' }).first().click(); + await test.step(`Create Org ${name}`, async () => { + let pm_locator = page.locator('a').filter({ hasText: 'Password Manager' }); + if( await pm_locator.count() > 0 ){ + pm_locator.first().click(); + } await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible(); await page.getByRole('link', { name: 'New organisation' }).click(); - await page.getByLabel('Organisation name (required)').fill(name); + await page.getByRole('textbox', { name: 'Organisation name * (required)', exact: true }).fill(name); await page.getByRole('button', { name: 'Submit' }).click(); await utils.checkNotification(page, 'Organisation created'); @@ -18,7 +21,7 @@ export async function policies(test, page: Page, name: string) { await test.step(`Navigate to ${name} policies`, async () => { await page.locator('a').filter({ hasText: 'Admin Console' }).first().click(); await page.locator('org-switcher').getByLabel(/Toggle collapse/).click(); - await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click(); + await page.locator('org-switcher > bit-nav-group > div > bit-nav-item').filter({ hasText: `${name}` }).first().click(); await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible(); await page.getByRole('button', { name: 'Toggle collapse Settings' }).click(); await page.getByRole('link', { name: 'Policies' }).click(); @@ -30,11 +33,11 @@ export async function members(test, page: Page, name: string) { await test.step(`Navigate to ${name} members`, async () => { await page.locator('a').filter({ hasText: 'Admin Console' }).first().click(); await page.locator('org-switcher').getByLabel(/Toggle collapse/).click(); - await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click(); + await page.locator('org-switcher > bit-nav-group > div > bit-nav-item').filter({ hasText: `${name}` }).first().click(); await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible(); - await page.locator('div').filter({ hasText: 'Members' }).nth(2).click(); + await page.getByRole('link', { name: 'Members' }).click(); await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'All' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Select all' })).toBeVisible(); }); } @@ -42,13 +45,13 @@ export async function invite(test, page: Page, name: string, email: string) { await test.step(`Invite ${email}`, async () => { await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible(); await page.getByRole('button', { name: 'Invite member' }).click(); - await page.getByLabel('Email (required)').fill(email); + await page.getByRole('textbox', { name: 'Email * (required)', exact: true }).fill(email); await page.getByRole('tab', { name: 'Collections' }).click(); await page.getByRole('combobox', { name: 'Permission' }).click(); await page.getByText('Edit items', { exact: true }).click(); - await page.getByLabel('Select collections').click(); - await page.getByText('Default collection').click(); - await page.getByRole('cell', { name: 'Collection', exact: true }).click(); + await page.getByRole('combobox', { name: 'Select collections' }).click(); + await page.getByLabel('Options List').getByText('Default collection').click(); + await page.getByRole('columnheader', { name: 'Collection', exact: true }).click(); await page.getByRole('button', { name: 'Save' }).click(); await utils.checkNotification(page, 'User(s) invited'); }); diff --git a/playwright/tests/setups/sso-teardown.ts b/playwright/tests/setups/sso-teardown.ts index 2899afff..22934b75 100644 --- a/playwright/tests/setups/sso-teardown.ts +++ b/playwright/tests/setups/sso-teardown.ts @@ -6,7 +6,7 @@ const utils = require('../../global-utils'); utils.loadEnv(); test('Keycloak teardown', async () => { - if( process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) { + if( process.env.PW_KEEP_SERVICE_RUNNING === "true" ) { console.log("Keep Keycloak running"); } else { console.log("Keycloak stopping"); diff --git a/playwright/tests/setups/sso.ts b/playwright/tests/setups/sso.ts index 6317f8b0..0ad0cffb 100644 --- a/playwright/tests/setups/sso.ts +++ b/playwright/tests/setups/sso.ts @@ -15,11 +15,8 @@ export async function logNewUser( options: { mailBuffer?: MailBuffer } = {} ) { await test.step(`Create user ${user.name}`, async () => { - await page.context().clearCookies(); - await test.step('Landing page', async () => { await utils.cleanLanding(page); - await page.locator("input[type=email].vw-email-sso").fill(user.email); await page.getByRole('button', { name: /Use single sign-on/ }).click(); }); @@ -33,26 +30,24 @@ export async function logNewUser( await test.step('Create Vault account', async () => { await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible(); - await page.getByLabel('Master password (required)', { exact: true }).fill(user.password); - await page.getByLabel('Confirm master password (').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); + await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(user.password); await page.getByRole('button', { name: 'Create account' }).click(); }); - await utils.checkNotification(page, 'Account successfully created!'); - await utils.checkNotification(page, 'Invitation accepted'); - - await utils.ignoreExtension(page); - await test.step('Default vault page', async () => { await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible(); }); + await utils.checkNotification(page, 'Account successfully created!'); + await utils.checkNotification(page, 'Invitation accepted'); + if( options.mailBuffer ){ let mailBuffer = options.mailBuffer; await test.step('Check emails', async () => { - await mailBuffer.expect((m) => m.subject === "Welcome"); await mailBuffer.expect((m) => m.subject.includes("New Device Logged")); + await mailBuffer.expect((m) => m.subject === "Welcome"); }); } }); @@ -69,16 +64,14 @@ export async function logUser( mailBuffer ?: MailBuffer, totp?: OTPAuth.TOTP, mail2fa?: boolean, + notNewDevice?: boolean, } = {} ) { let mailBuffer = options.mailBuffer; await test.step(`Log user ${user.email}`, async () => { - await page.context().clearCookies(); - await test.step('Landing page', async () => { await utils.cleanLanding(page); - await page.locator("input[type=email].vw-email-sso").fill(user.email); await page.getByRole('button', { name: /Use single sign-on/ }).click(); }); @@ -117,14 +110,12 @@ export async function logUser( await page.getByRole('button', { name: 'Unlock' }).click(); }); - await utils.ignoreExtension(page); - await test.step('Default vault page', async () => { await expect(page).toHaveTitle(/Vaultwarden Web/); await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible(); }); - if( mailBuffer ){ + if( mailBuffer && !options.notNewDevice ){ await test.step('Check email', async () => { await mailBuffer.expect((m) => m.subject.includes("New Device Logged")); }); diff --git a/playwright/tests/setups/user.ts b/playwright/tests/setups/user.ts index 395196ae..3d3990e9 100644 --- a/playwright/tests/setups/user.ts +++ b/playwright/tests/setups/user.ts @@ -3,6 +3,7 @@ import { expect, type Browser, Page } from '@playwright/test'; import { type MailBuffer } from 'maildev'; import * as utils from '../../global-utils'; +import { retrieveEmailCode } from './2fa'; export async function createAccount(test, page: Page, user: { email: string, name: string, password: string }, mailBuffer?: MailBuffer) { await test.step(`Create user ${user.name}`, async () => { @@ -17,12 +18,11 @@ export async function createAccount(test, page: Page, user: { email: string, nam await page.getByRole('button', { name: 'Continue' }).click(); // Vault finish Creation - await page.getByLabel('Master password (required)', { exact: true }).fill(user.password); - await page.getByLabel('Confirm master password (').fill(user.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); + await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(user.password); await page.getByRole('button', { name: 'Create account' }).click(); await utils.checkNotification(page, 'Your new account has been created') - await utils.ignoreExtension(page); // We are now in the default vault page await expect(page).toHaveTitle('Vaults | Vaultwarden Web'); @@ -35,7 +35,16 @@ export async function createAccount(test, page: Page, user: { email: string, nam }); } -export async function logUser(test, page: Page, user: { email: string, password: string }, mailBuffer?: MailBuffer) { +export async function logUser( + test, + page: Page, + user: { email: string, password: string }, + options: { + mailBuffer ?: MailBuffer, + mail2fa?: boolean, + notNewDevice?: boolean, + } = {} +) { await test.step(`Log user ${user.email}`, async () => { await utils.cleanLanding(page); @@ -43,16 +52,23 @@ export async function logUser(test, page: Page, user: { email: string, password: await page.getByRole('button', { name: 'Continue' }).click(); // Unlock page - await page.getByLabel('Master password').fill(user.password); - await page.getByRole('button', { name: 'Log in with master password' }).click(); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(user.password); + await page.getByRole('button', { name: 'Log in', exact: true }).click(); - await utils.ignoreExtension(page); + if( options.mail2fa ){ + await test.step('2FA check', async () => { + await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible(); + let code = await retrieveEmailCode(test, page, options.mailBuffer); + await page.getByLabel(/Verification code/).fill(code); + await page.getByRole('button', { name: 'Continue' }).click(); + }); + } // We are now in the default vault page await expect(page).toHaveTitle(/Vaultwarden Web/); - if( mailBuffer ){ - await mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox"); + if( options.mailBuffer && !options.notNewDevice ){ + await options.mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox"); } }); } diff --git a/playwright/tests/sso_login.smtp.spec.ts b/playwright/tests/sso_login.smtp.spec.ts index 7a615cd6..1f5c9361 100644 --- a/playwright/tests/sso_login.smtp.spec.ts +++ b/playwright/tests/sso_login.smtp.spec.ts @@ -1,6 +1,7 @@ import { test, expect, type TestInfo } from '@playwright/test'; import { MailDev } from 'maildev'; +import * as admin from "./setups/admin"; import { logNewUser, logUser } from './setups/sso'; import { activateEmail, disableEmail } from './setups/2fa'; import * as utils from "../global-utils"; @@ -19,7 +20,7 @@ test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { await utils.startVault(browser, testInfo, { SSO_ENABLED: true, - SSO_ONLY: false, + SSO_ONLY: true, SMTP_HOST: process.env.MAILDEV_HOST, SMTP_FROM: process.env.PW_SMTP_FROM, }); @@ -32,22 +33,64 @@ test.afterAll('Teardown', async ({}) => { } }); -test('Create and activate 2FA', async ({ page }) => { +test('2FA email', async ({ page }) => { + const mailBuffer = mailserver.buffer(users.user1.email); await logNewUser(test, page, users.user1, {mailBuffer: mailBuffer}); await activateEmail(test, page, users.user1, mailBuffer); - mailBuffer.close(); -}); - -test('Log and disable', async ({ page }) => { - const mailBuffer = mailserver.buffer(users.user1.email); - - await logUser(test, page, users.user1, {mailBuffer: mailBuffer, mail2fa: true}); + await logUser(test, page, users.user1, {mailBuffer: mailBuffer, mail2fa: true, notNewDevice: true}); await disableEmail(test, page, users.user1); mailBuffer.close(); }); + + +test('Admin invite', async ({ page }) => { + const mailBuffer = mailserver.buffer(users.user2.email); + + await admin.login(test, page); + await admin.invite(test, page, users.user2.email); + + + const link = await test.step('Extract email link', async () => { + const invited = await mailBuffer.expect((m) => m.subject === "Join Vaultwarden"); + await page.setContent(invited.html); + return await page.getByTestId("invite").getAttribute("href"); + }); + + await test.step('Redirect to Keycloak', async () => { + await page.goto(link); + }); + + await test.step('Keycloak login', async () => { + await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible(); + await page.getByLabel(/Username/).fill(users.user2.name); + await page.getByLabel('Password', { exact: true }).fill(users.user2.password); + await page.getByRole('button', { name: 'Sign In' }).click(); + }); + + await test.step('Create Vault account', async () => { + await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible(); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password); + await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password); + await page.getByRole('button', { name: 'Create account' }).click(); + }); + + await test.step('Default vault page', async () => { + await expect(page).toHaveTitle('Vaults | Vaultwarden Web'); + + await utils.checkNotification(page, 'Account successfully created!'); + await utils.checkNotification(page, 'Invitation accepted'); + }); + + await test.step('Check mails', async () => { + await mailBuffer.expect((m) => m.subject.includes("New Device Logged")); + await mailBuffer.expect((m) => m.subject === "Welcome"); + }); + + mailBuffer.close(); +}); diff --git a/playwright/tests/sso_login.spec.ts b/playwright/tests/sso_login.spec.ts index 8a1bb9ab..e93aab14 100644 --- a/playwright/tests/sso_login.spec.ts +++ b/playwright/tests/sso_login.spec.ts @@ -33,8 +33,8 @@ test('Non SSO login', async ({ page }) => { await page.getByRole('button', { name: 'Other' }).click(); // Unlock page - await page.getByLabel('Master password').fill(users.user1.password); - await page.getByRole('button', { name: 'Log in with master password' }).click(); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user1.password); + await page.getByRole('button', { name: 'Log in', exact: true }).click(); // We are now in the default vault page await expect(page).toHaveTitle(/Vaultwarden Web/); @@ -58,6 +58,7 @@ test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) = // Landing page await page.goto('/'); + await page.locator("input[type=email].vw-email-sso").fill(users.user1.email); // Check that SSO login is available await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1); @@ -66,7 +67,6 @@ test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) = await expect(page.getByRole('button', { name: 'Other' })).toHaveCount(0); }); - test('No SSO login', async ({ page }, testInfo: TestInfo) => { await utils.restartVault(page, testInfo, { SSO_ENABLED: false @@ -74,12 +74,14 @@ test('No SSO login', async ({ page }, testInfo: TestInfo) => { // Landing page await page.goto('/'); + await page.getByLabel(/Email address/).fill(users.user1.email); // No SSO button (rely on a correct selector checked in previous test) + await page.getByLabel('Master password'); await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0); // Can continue to Master password await page.getByLabel(/Email address/).fill(users.user1.email); await page.getByRole('button', { name: 'Continue' }).click(); - await expect(page.getByRole('button', { name: 'Log in with master password' })).toHaveCount(1); + await expect(page.getByRole('button', { name: 'Log in' })).toHaveCount(1); }); diff --git a/playwright/tests/sso_organization.smtp.spec.ts b/playwright/tests/sso_organization.smtp.spec.ts index 92813f72..eef4f83d 100644 --- a/playwright/tests/sso_organization.smtp.spec.ts +++ b/playwright/tests/sso_organization.smtp.spec.ts @@ -67,17 +67,16 @@ test('invited with new account', async ({ page }) => { await test.step('Create Vault account', async () => { await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible(); - await page.getByLabel('Master password (required)', { exact: true }).fill(users.user2.password); - await page.getByLabel('Confirm master password (').fill(users.user2.password); + await page.getByRole('textbox', { name: 'Master password * (required)', exact: true }).fill(users.user2.password); + await page.getByRole('textbox', { name: 'Confirm master password * (' }).fill(users.user2.password); await page.getByRole('button', { name: 'Create account' }).click(); - - await utils.checkNotification(page, 'Account successfully created!'); - await utils.checkNotification(page, 'Invitation accepted'); - await utils.ignoreExtension(page); }); await test.step('Default vault page', async () => { await expect(page).toHaveTitle(/Vaultwarden Web/); + + await utils.checkNotification(page, 'Account successfully created!'); + await utils.checkNotification(page, 'Invitation accepted'); }); await test.step('Check mails', async () => { @@ -95,6 +94,7 @@ test('invited with existing account', async ({ page }) => { await test.step('Redirect to Keycloak', async () => { await page.goto(link); + await page.getByRole('button', { name: /Use single sign-on/ }).click(); }); await test.step('Keycloak login', async () => { @@ -108,13 +108,11 @@ test('invited with existing account', async ({ page }) => { await expect(page).toHaveTitle('Vaultwarden Web'); await page.getByLabel('Master password').fill(users.user3.password); await page.getByRole('button', { name: 'Unlock' }).click(); - - await utils.checkNotification(page, 'Invitation accepted'); - await utils.ignoreExtension(page); }); await test.step('Default vault page', async () => { await expect(page).toHaveTitle(/Vaultwarden Web/); + await utils.checkNotification(page, 'Successfully accepted your invitation'); }); await test.step('Check mails', async () => { diff --git a/playwright/tests/sso_organization.spec.ts b/playwright/tests/sso_organization.spec.ts index c1238d45..ee7e28f6 100644 --- a/playwright/tests/sso_organization.spec.ts +++ b/playwright/tests/sso_organization.spec.ts @@ -49,7 +49,7 @@ test('Organization is visible', async ({ page }) => { await expect(page.getByLabel('Filter: Default collection')).toBeVisible(); }); -test('Enforce password policy', async ({ page }) => { +test('Activate password policy', async ({ page }) => { await logUser(test, page, users.user1); await orgs.policies(test, page, '/Test'); @@ -61,16 +61,27 @@ test('Enforce password policy', async ({ page }) => { await page.getByRole('button', { name: 'Save' }).click(); await utils.checkNotification(page, 'Edited policy Master password requirements.'); }); - - await utils.logout(test, page, users.user1); - - await test.step(`Unlock trigger policy`, async () => { - await page.locator("input[type=email].vw-email-sso").fill(users.user1.email); - await page.getByRole('button', { name: 'Use single sign-on' }).click(); - - await page.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password); - await page.getByRole('button', { name: 'Unlock' }).click(); - - await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible(); - }); +}); + +test('Unlock trigger policyy', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded' }); + + await page.locator("input[type=email].vw-email-sso").fill(users.user2.email); + await page.getByRole('button', { name: /Use single sign-on/ }).click(); + + await test.step('Keycloak login', async () => { + await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible(); + await page.getByLabel(/Username/).fill(users.user2.name); + await page.getByLabel('Password', { exact: true }).fill(users.user2.password); + await page.getByRole('button', { name: 'Sign In' }).click(); + }); + + await test.step('Unlock vault', async () => { + await expect(page).toHaveTitle('Vaultwarden Web'); + await expect(page.getByRole('heading', { name: 'Your vault is locked' })).toBeVisible(); + await page.getByLabel('Master password').fill(users.user2.password); + await page.getByRole('button', { name: 'Unlock' }).click(); + }); + + await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible(); }); diff --git a/src/api/admin.rs b/src/api/admin.rs index 7037bfb1..48f36afd 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -716,6 +716,36 @@ fn web_vault_compare(active: &str, latest: &str) -> i8 { } } +fn check_template_overrides() -> Vec<&'static str> { + let template_folder = std::path::PathBuf::from(CONFIG.templates_folder()); + let mut overrides = Vec::new(); + for folder in ["admin", "email", "scss"] { + if folder_has_hbs_files(&template_folder.join(folder)) { + overrides.push(folder); + } + } + + if folder_has_hbs_files(&template_folder) { + overrides.push("other"); + } + + overrides +} + +fn folder_has_hbs_files(dir: &std::path::Path) -> bool { + let Ok(files) = std::fs::read_dir(dir) else { + // No files in this directory at all, so we can return false + return false; + }; + + files.flatten().any(|f| { + // Validate if it is a file and if it has the `.hbs` extension and starts with a-z or 0-9 + f.file_type().is_ok_and(|t| t.is_file()) + && f.path().extension().is_some_and(|e| e.eq_ignore_ascii_case("hbs")) + && f.file_name().to_str().is_some_and(|n| n.starts_with(|c: char| c.is_ascii_alphanumeric())) + }) +} + #[get("/diagnostics")] async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult> { use chrono::prelude::*; @@ -770,6 +800,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A "db_version": get_sql_server_version(&conn).await, "admin_url": format!("{}/diagnostics", admin_url()), "overrides": &CONFIG.get_overrides().join(", "), + "template_overrides": check_template_overrides().join(", "), "invalid_feature_flags": invalid_feature_flags, "host_arch": env::consts::ARCH, "host_os": env::consts::OS, diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 0b834e49..217ae3dc 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -871,7 +871,7 @@ async fn put_collections_admin( headers: Headers, conn: DbConn, nt: Notify<'_>, -) -> EmptyResult { +) -> JsonResult { post_collections_admin(cipher_id, data, headers, conn, nt).await } @@ -882,7 +882,7 @@ async fn post_collections_admin( headers: Headers, conn: DbConn, nt: Notify<'_>, -) -> EmptyResult { +) -> JsonResult { let data: CollectionsAdminData = data.into_inner(); let Some(cipher) = Cipher::find_by_uuid(&cipher_id, &conn).await else { @@ -941,7 +941,7 @@ async fn post_collections_admin( ) .await; - Ok(()) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::Organization, &conn).await?)) } #[derive(Deserialize)] diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index a5ae50a4..e6a184dd 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -238,7 +238,7 @@ fn config() -> Json { "disableUserRegistration": CONFIG.is_signup_disabled(), // When enabled, this setting signals to clients that onboarding interstitials // (post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals) should be suppressed - "suppressOnboardingInterstitials": false + "suppressOnboardingInterstitials": CONFIG.client_suppress_onboarding(), }, "environment": { "vault": domain, diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs index 08e8d269..eb3d6dfd 100644 --- a/src/api/core/two_factor/yubikey.rs +++ b/src/api/core/two_factor/yubikey.rs @@ -1,6 +1,10 @@ use rocket::{Route, serde::json::Json}; use serde_json::Value; -use yubico::{config::Config, verify_async}; +use yubico_ng::{ + Verifier, YubicoError, + config::Config, + transport::{AsyncTransport, Response}, +}; use crate::{ CONFIG, @@ -14,12 +18,39 @@ use crate::{ models::{EventType, TwoFactor, TwoFactorType}, }, error::{Error, MapResult}, + http_client, }; pub fn routes() -> Vec { routes![generate_yubikey, activate_yubikey, activate_yubikey_put,] } +struct HttpClientTransport { + client: reqwest::Client, +} + +impl HttpClientTransport { + fn new() -> Result { + http_client::get_reqwest_client_builder(false).redirect(reqwest::redirect::Policy::none()).build().map( + |client| Self { + client, + }, + ) + } +} + +impl AsyncTransport for HttpClientTransport { + type Error = YubicoError; + + async fn yubico_get(&self, url: &str) -> Result { + let response = self.client.get(url).send().await.map_err(YubicoError::transport)?; + Ok(Response { + status: response.status().as_u16(), + body: response.text().await.map_err(YubicoError::transport)?, + }) + } +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct EnableYubikeyData { @@ -44,8 +75,7 @@ pub struct YubikeyMetadata { fn parse_yubikeys(data: &EnableYubikeyData) -> Vec { let data_keys = [&data.key1, &data.key2, &data.key3, &data.key4, &data.key5]; - - data_keys.into_iter().flatten().cloned().collect() + data_keys.into_iter().flatten().filter(|e| !e.is_empty()).cloned().collect() } fn jsonify_yubikeys(yubikeys: Vec) -> Value { @@ -73,13 +103,15 @@ fn get_yubico_credentials() -> Result<(String, String), Error> { async fn verify_yubikey_otp(otp: String) -> EmptyResult { let (yubico_id, yubico_secret) = get_yubico_credentials()?; - let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret); - - match CONFIG.yubico_server() { - Some(server) => verify_async(otp, config.set_api_hosts(vec![server])).await, - None => verify_async(otp, config).await, + let mut config = Config::default().set_client_id(yubico_id).set_key(yubico_secret)?; + if let Some(yubico_server) = CONFIG.yubico_server() { + config = config.set_api_host(yubico_server); } - .map_res("Failed to verify OTP") + + let client = HttpClientTransport::new()?; + let verifier = Verifier::with_client(config, client)?; + + verifier.verify(otp).await.map_res("Failed to verify OTP") } #[post("/two-factor/get-yubikey", data = "")] @@ -137,10 +169,9 @@ async fn activate_yubikey(data: Json, headers: Headers, conn: let yubikeys = parse_yubikeys(&data); if yubikeys.is_empty() { - return Ok(Json(json!({ - "enabled": false, - "object": "twoFactorU2f", - }))); + // Return an error to prevent saving empty keys which would cause users not being able to login anymore. + // To remove all keys users should click the `Deactivate all keys` button + err!("A key is required."); } // Ensure they are valid OTPs diff --git a/src/api/mod.rs b/src/api/mod.rs index 05c4215d..9a79ce95 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -30,7 +30,7 @@ pub use crate::api::{ }, web::catchers as web_catchers, web::routes as web_routes, - web::static_files, + web::{invalidate_css_cache, static_files}, }; use crate::{ CONFIG, diff --git a/src/api/web.rs b/src/api/web.rs index 5bd4c85d..a7eca9fc 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -1,4 +1,7 @@ -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + sync::{Arc, RwLock}, +}; use rocket::{ Catcher, Route, @@ -13,12 +16,13 @@ use crate::{ CONFIG, api::{ApiResult, EmptyResult, core::now}, auth::decode_file_download, + crypto::sha256_hex, db::{ DbConn, models::{AttachmentId, CipherId}, }, error::Error, - util::Cached, + util::{Cached, EtagCached}, }; pub fn routes() -> Vec { @@ -63,8 +67,27 @@ fn not_found() -> ApiResult> { Ok(Html(text)) } +struct CssCache { + css: String, + etag: String, +} + +static CSS_CACHE: RwLock>> = RwLock::new(None); + +pub fn invalidate_css_cache() { + *CSS_CACHE.write().unwrap() = None; +} + #[get("/css/vaultwarden.css")] -fn vaultwarden_css() -> Cached> { +fn vaultwarden_css() -> EtagCached> { + // If reload_templates is false, and we already have the CSS Cached, return this + if !CONFIG.reload_templates() + && let Some(cached) = CSS_CACHE.read().unwrap().as_ref() + { + return EtagCached::new(Css(cached.css.clone()), &cached.etag); + } + + // Else, there is either no cache, or reload_templates is true and we need to rebuild the CSS let css_options = json!({ "emergency_access_allowed": CONFIG.emergency_access_allowed(), "load_user_scss": true, @@ -112,8 +135,18 @@ fn vaultwarden_css() -> Cached> { } }; - // Cache for one day should be enough and not too much - Cached::ttl(Css(css), 86_400, false) + let etag = sha256_hex(css.as_bytes()); + let cached = Arc::new(CssCache { + css, + etag, + }); + + if !CONFIG.reload_templates() { + *CSS_CACHE.write().unwrap() = Some(Arc::clone(&cached)); + } + + // Etag Caching will let the browser send us an etag to verify and send new content if needed + EtagCached::new(Css(cached.css.clone()), &cached.etag) } #[get("/")] diff --git a/src/config.rs b/src/config.rs index c4457478..d5b50146 100644 --- a/src/config.rs +++ b/src/config.rs @@ -659,6 +659,11 @@ make_config! { events_days_retain: i64, false, option; }, + client { + /// Control whether clients onboarding interstitials are suppressed |> post-login welcome dialogs, extension install prompts, setup extension redirects, and premium upsell modals + client_suppress_onboarding: bool, true, def, false; + }, + /// Advanced settings advanced { /// Client IP header |> If not present, the remote IP is used. @@ -1501,6 +1506,9 @@ impl Config { let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?; operator.write(&CONFIG_FILENAME, config_str).await?; + // Invalidate CSS Cache because several config items might have impact on the rendered CSS + crate::api::invalidate_css_cache(); + Ok(()) } @@ -1583,6 +1591,9 @@ impl Config { writer._overrides = Vec::new(); } + // Invalidate CSS Cache because several config items might have impact on the rendered CSS + crate::api::invalidate_css_cache(); + Ok(()) } diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 3a6b610c..86cbf5d0 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -298,12 +298,16 @@ impl Event { ) -> Vec { conn.run(move |conn| { event::table - .inner_join(users_organizations::table.on(users_organizations::uuid.eq(member_uuid))) + .inner_join( + users_organizations::table + .on(users_organizations::uuid.eq(member_uuid).and(users_organizations::org_uuid.eq(org_uuid))), + ) .filter(event::org_uuid.eq(org_uuid)) .filter(event::event_date.between(start, end)) .filter( - event::user_uuid - .eq(users_organizations::user_uuid.nullable()) + event::org_user_uuid + .eq(member_uuid) + .or(event::user_uuid.eq(users_organizations::user_uuid.nullable())) .or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable())), ) .select(event::all_columns) diff --git a/src/error.rs b/src/error.rs index ecbc8199..d90c38e3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -58,7 +58,7 @@ use serde_json::{Error as SerdeErr, Value}; use std::io::Error as IoErr; use std::time::SystemTimeError as TimeErr; use webauthn_rs::prelude::WebauthnError as WebauthnErr; -use yubico::yubicoerror::YubicoError as YubiErr; +use yubico_ng::error::YubicoError as YubiErr; #[derive(Serialize)] pub struct Empty {} diff --git a/src/static/scripts/admin.js b/src/static/scripts/admin.js index 3f6bb1df..fa949a40 100644 --- a/src/static/scripts/admin.js +++ b/src/static/scripts/admin.js @@ -1,6 +1,5 @@ "use strict"; -/* eslint-env es2017, browser */ -/* exported BASE_URL, _post _delete */ +/* exported BASE_URL, _post, _delete */ function getBaseUrl() { // If the base URL is `https://vaultwarden.example.com/base/path/admin/`, diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js index 2cff4410..ae4d4235 100644 --- a/src/static/scripts/admin_diagnostics.js +++ b/src/static/scripts/admin_diagnostics.js @@ -1,5 +1,4 @@ "use strict"; -/* eslint-env es2017, browser */ /* global BASE_URL:readable, bootstrap:readable */ var dnsCheck = false; @@ -80,37 +79,44 @@ async function generateSupportString(event, dj) { event.preventDefault(); event.stopPropagation(); + // Health check Markdown emoji, if something is a failure or not + const chk = v => v ? "true :white_check_mark:" : "false :x:"; + // Yes/No Markdown emoji, if something is not a failure, but just yes or no + const yn = v => v ? "yes :heavy_plus_sign:" : "no :heavy_minus_sign:"; + + const template_overrides = dj.template_overrides !== "" ? ` (${dj.template_overrides})` : ""; let supportString = "### Your environment (Generated via diagnostics page)\n\n"; supportString += `* Vaultwarden version: v${dj.current_release}\n`; supportString += `* Web-vault version: v${dj.active_web_release}\n`; supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`; - supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`; + supportString += `* Running within a container: ${yn(dj.running_within_container)} (Base: ${dj.container_base_image})\n`; supportString += `* Database type: ${dj.db_type}\n`; supportString += `* Database version: ${dj.db_version}\n`; - supportString += `* Uses config.json: ${dj.overrides !== ""}\n`; - supportString += `* Uses a reverse proxy: ${dj.ip_header_exists}\n`; + supportString += `* Uses config.json: ${yn(dj.overrides !== "")}\n`; + supportString += `* Uses custom templates: ${yn(dj.template_overrides !== "")}${template_overrides}\n`; + supportString += `* Uses a reverse proxy: ${yn(dj.ip_header_exists)}\n`; if (dj.ip_header_exists) { - supportString += `* IP Header check: ${dj.ip_header_match} (${dj.ip_header_name})\n`; + supportString += `* IP Header check: ${chk(dj.ip_header_match)} (${dj.ip_header_name})\n`; } - supportString += `* Internet access: ${dj.has_http_access}\n`; - supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`; - supportString += `* DNS Check: ${dnsCheck}\n`; + supportString += `* Internet access: ${chk(dj.has_http_access)}\n`; + supportString += `* Internet access via a proxy: ${yn(dj.uses_proxy)}\n`; + supportString += `* DNS Check: ${chk(dnsCheck)}\n`; if (dj.tz_env !== "") { supportString += `* TZ environment: ${dj.tz_env}\n`; } - supportString += `* Browser/Server Time Check: ${timeCheck}\n`; - supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`; - supportString += `* Domain Configuration Check: ${domainCheck}\n`; - supportString += `* HTTPS Check: ${httpsCheck}\n`; + supportString += `* Browser/Server Time Check: ${chk(timeCheck)}\n`; + supportString += `* Server/NTP Time Check: ${chk(ntpTimeCheck)}\n`; + supportString += `* Domain Configuration Check: ${chk(domainCheck)}\n`; + supportString += `* HTTPS Check: ${chk(httpsCheck)}\n`; if (dj.enable_websocket) { - supportString += `* Websocket Check: ${websocketCheck}\n`; + supportString += `* Websocket Check: ${chk(websocketCheck)}\n`; } else { supportString += "* Websocket Check: disabled\n"; } - supportString += `* HTTP Response Checks: ${httpResponseCheck}\n`; + supportString += `* HTTP Response Checks: ${chk(httpResponseCheck)}\n`; if (dj.invalid_feature_flags != "") { - supportString += `* Invalid feature flags: true\n`; + supportString += "* Invalid feature flags: true\n"; } const jsonResponse = await fetch(`${BASE_URL}/admin/diagnostics/config`, { diff --git a/src/static/scripts/admin_organizations.js b/src/static/scripts/admin_organizations.js index c885344e..33314ad7 100644 --- a/src/static/scripts/admin_organizations.js +++ b/src/static/scripts/admin_organizations.js @@ -1,6 +1,5 @@ "use strict"; -/* eslint-env es2017, browser, jquery */ -/* global _post:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ +/* global jQuery, _post:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ function deleteOrganization(event) { event.preventDefault(); diff --git a/src/static/scripts/admin_settings.js b/src/static/scripts/admin_settings.js index 3d61a508..9061719e 100644 --- a/src/static/scripts/admin_settings.js +++ b/src/static/scripts/admin_settings.js @@ -1,5 +1,4 @@ "use strict"; -/* eslint-env es2017, browser */ /* global _post:readable, BASE_URL:readable */ function smtpTest(event) { diff --git a/src/static/scripts/admin_users.js b/src/static/scripts/admin_users.js index 99e39aab..a2a643c3 100644 --- a/src/static/scripts/admin_users.js +++ b/src/static/scripts/admin_users.js @@ -1,6 +1,5 @@ "use strict"; -/* eslint-env es2017, browser, jquery */ -/* global _post:readable, _delete:readable BASE_URL:readable, reload:readable, jdenticon:readable */ +/* global jQuery, _post:readable, _delete:readable, BASE_URL:readable, reload:readable, jdenticon:readable */ function deleteUser(event) { event.preventDefault(); diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index ddde389b..0c889353 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -77,6 +77,16 @@ No {{/unless}} +
Uses custom templates
+
+ {{#if page_data.template_overrides}} + Yes + Details + {{/if}} + {{#unless page_data.template_overrides}} + No + {{/unless}} +
Uses a reverse proxy
{{#if page_data.ip_header_exists}} diff --git a/src/storage.rs b/src/storage.rs index ac88d026..689be302 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -67,7 +67,7 @@ pub(crate) fn operator_for_path(path: &str) -> Result bool { diff --git a/src/util.rs b/src/util.rs index 91f075d1..0e8a93e4 100644 --- a/src/util.rs +++ b/src/util.rs @@ -257,6 +257,44 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache } } +pub struct EtagCached { + response: R, + etag: String, +} + +impl EtagCached { + /// An `etag` response should always be quoted + pub fn new(response: R, etag: &str) -> Self { + Self { + response, + etag: format!("\"{etag}\""), + } + } +} + +impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for EtagCached { + fn respond_to(self, request: &'r Request<'_>) -> response::Result<'static> { + // Check and validate a `If-None-Match` ETag header + // Multiple tags could be returned for the same URI if the browser has multiple versions cached + // Also, weak tags are prefixed with `W/`, but ETags are always weak, so just strip it too before comparing + let etag_matches = request + .headers() + .get_one("If-None-Match") + .is_some_and(|v| v.split(',').any(|t| t.trim().trim_start_matches("W/") == self.etag)); + + let mut res = if etag_matches { + Response::build().status(Status::NotModified).ok()? + } else { + self.response.respond_to(request)? + }; + + // Both 200 (OK) and 304 (Not Modified) need to return the etag and cache-control + res.set_raw_header("Etag", self.etag); + res.set_raw_header("Cache-Control", "public, no-cache"); + Ok(res) + } +} + // Log all the routes from the main paths list, and the attachments endpoint // Effectively ignores, any static file route, and the alive endpoint const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"];