diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..f329996b91a25789e77a6cb2f3da06cad4f1b2ef
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,29 @@
+# https://EditorConfig.org
+
+root = true
+
+[*]
+indent_style = space
+# Reduce tab size on GitHub
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+block_comment_start = /*
+block_comment = *
+block_comment_end = */
+
+[{*.yml,*.yaml}]
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab
+
+# Generated file
+[infection.txt]
+indent_size = unset
+trim_trailing_whitespace = unset
diff --git a/.gitattributes b/.gitattributes
index cea09fccdf7c684b0c3a5135ae68018a3f159d0d..de25e8fd72dbd0716f9370206a6b4b83c075e456 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -3,14 +3,14 @@
 /.github export-ignore
 /doc export-ignore
 /tests export-ignore
+/.editorconfig export-ignore
 /.gitattributes export-ignore
 /.gitignore export-ignore
 /CODE_OF_CONDUCT.md export-ignore
+/deptrac.yaml export-ignore
 /ecs.php export-ignore
 /infection.json.dist export-ignore
 /Makefile export-ignore
 /phpstan.neon export-ignore
 /phpunit.xml.dist export-ignore
-/README.md export-ignore
 /rector.php export-ignore
-/SECURITY.md export-ignore
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 2b7c6aaa23db70c557a54a3c50e7ad45d8b1f7fd..7cc67ce37ec5684052f27eb1b416515a419804d7 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -11,9 +11,14 @@ Few rules to ease code reviews and merges:
 - You MUST write (or update) unit tests when bugs are fixed or features are added.
 - You SHOULD write documentation.
 
-To contribute use [Pull Requests](https://help.github.com/articles/using-pull-requests), please, write commit messages that make sense, and rebase your branch before submitting your PR.
+We use [Git-Flow](http://jeffkreeftmeijer.com/2010/why-arent-you-using-git-flow/) to automate our git branching
+workflow.
 
-May be asked to squash your commits too. This is used to "clean" your Pull Request before merging it, avoiding commits such as fix tests, fix 2, fix 3, etc.
+To contribute use [Pull Requests](https://help.github.com/articles/using-pull-requests), please, write commit messages
+that make sense, and rebase your branch before submitting your PR.
+
+May be asked to squash your commits too. This is used to "clean" your Pull Request before merging it, avoiding commits
+such as fix tests, fix 2, fix 3, etc.
 
 Run test suite
 ------------
@@ -21,7 +26,3 @@ Run test suite
 * install composer: `curl -s http://getcomposer.org/installer | php`
 * install dependencies: `php composer.phar install`
 * run tests: `vendor/bin/phpunit`
-* check and fix coding standards:
-  * `vendor/bin/phpstan analyse`
-  * `vendor/bin/rector process`
-  * `vendor/bin/ecs check --fix`
diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yaml b/.github/ISSUE_TEMPLATE/1_Bug_report.yaml
index 841cfdc01291a0ce2c7febbf5d53745c5ccb6ec7..d3f26cf6bd326766d4222d2889b286c5de38be15 100644
--- a/.github/ISSUE_TEMPLATE/1_Bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yaml
@@ -1,43 +1,42 @@
 name: 🐛 Bug Report
 description: ⚠️ NEVER report security issues, email security AT spomky-labs.com instead
 labels: Bug
-
 body:
- - type: input
-   id: affected-versions
-   attributes:
-    label: Version(s) affected
-    placeholder: x.y.z
-   validations:
-    required: true
- - type: textarea
-   id: description
-   attributes:
-    label: Description
-    description: A clear and concise description of the problem
-   validations:
-    required: true
- - type: textarea
-   id: how-to-reproduce
-   attributes:
-    label: How to reproduce
-    description: |
-     ⚠️  This is the most important part of the report ⚠️
-     Without a way to easily reproduce your issue, there is little chance we will be able to help you and work on a fix.
-     Please, take the time to show us some code and/or config that is needed for others to reproduce the problem easily.
-     Most of the time, creating a "bug reproducer" is the best way to help us and increases the chances someone
-     will have a look at it.
-   validations:
-    required: true
- - type: textarea
-   id: possible-solution
-   attributes:
-    label: Possible Solution
-    description: |
-     Optional: only if you have suggestions on a fix/reason for the bug
-     Don't hesitate to create a pull request with your solution, it helps get faster feedback.
- - type: textarea
-   id: additional-context
-   attributes:
-    label: Additional Context
-    description: "Optional: any other context about the problem: log messages, screenshots, etc."
+  - type: input
+    id: affected-versions
+    attributes:
+      label: Version(s) affected
+      placeholder: x.y.z
+    validations:
+      required: true
+  - type: textarea
+    id: description
+    attributes:
+      label: Description
+      description: A clear and concise description of the problem
+    validations:
+      required: true
+  - type: textarea
+    id: how-to-reproduce
+    attributes:
+      label: How to reproduce
+      description: |
+        ⚠️  This is the most important part of the report ⚠️
+        Without a way to easily reproduce your issue, there is little chance we will be able to help you and work on a fix.
+        Please, take the time to show us some code and/or config that is needed for others to reproduce the problem easily.
+        Most of the time, creating a "bug reproducer" is the best way to help us and increases the chances someone
+        will have a look at it.
+    validations:
+      required: true
+  - type: textarea
+    id: possible-solution
+    attributes:
+      label: Possible Solution
+      description: |
+        Optional: only if you have suggestions on a fix/reason for the bug
+        Don't hesitate to create a pull request with your solution, it helps get faster feedback.
+  - type: textarea
+    id: additional-context
+    attributes:
+      label: Additional Context
+      description: "Optional: any other context about the problem: log messages, screenshots, etc."
diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.yaml b/.github/ISSUE_TEMPLATE/2_Feature_request.yaml
index bd300eb1e82b265ed3b47052863d05504e029a75..52b3de7906c281f6e7026a0fb807ea171013ab2f 100644
--- a/.github/ISSUE_TEMPLATE/2_Feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/2_Feature_request.yaml
@@ -1,17 +1,17 @@
 name: 🚀 Feature Request
 description: RFC and ideas for new features and improvements
 body:
-    - type: textarea
-      id: description
-      attributes:
-          label: Description
-          description: A clear and concise description of the new feature
-      validations:
-          required: true
-    - type: textarea
-      id: example
-      attributes:
-          label: Example
-          description: |
-              A simple example of the new feature in action (include PHP code, YAML config, etc.)
-              If the new feature changes an existing feature, include a simple before/after comparison.
+  - type: textarea
+    id: description
+    attributes:
+      label: Description
+      description: A clear and concise description of the new feature
+    validations:
+      required: true
+  - type: textarea
+    id: example
+    attributes:
+      label: Example
+      description: |
+        A simple example of the new feature in action (include PHP code, YAML config, etc.)
+        If the new feature changes an existing feature, include a simple before/after comparison.
diff --git a/.github/ISSUE_TEMPLATE/3_Documentation.yaml b/.github/ISSUE_TEMPLATE/3_Documentation.yaml
index 7a3dc1785d3ccbfe76cb1f3dc596ce18fb39e8d1..1c5ac8989090261cb0150b400740dfe255226e0a 100644
--- a/.github/ISSUE_TEMPLATE/3_Documentation.yaml
+++ b/.github/ISSUE_TEMPLATE/3_Documentation.yaml
@@ -1,10 +1,10 @@
 name: 📖 Documentation Issue
 description: To report typo or obsolete section in the documentation
 body:
-    - type: textarea
-      id: description
-      attributes:
-          label: Description
-          description: A clear and concise description of the error you found in the documentation
-      validations:
-          required: true
+  - type: textarea
+    id: description
+    attributes:
+      label: Description
+      description: A clear and concise description of the error you found in the documentation
+    validations:
+      required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 11812417c42426de5865c54614713516f84fadfb..6d11ce273e77fdb97b79626a96f290ee5e4efea7 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,8 @@
 blank_issues_enabled: false
 contact_links:
-    - name: Support Question
-      url: https://spomky-labs.com/contact/
-      about: We use GitHub issues only to discuss about bugs and new features. For this kind of questions about using the library, please use Stackoverflow (or similar) or send a quote request at https://spomky-labs.com/contact/
+  - name: Support Question
+    url: https://spomky-labs.com/contact/
+    about:|
+    We use GitHub issues only to discuss about bugs and new features.
+    For this kind of questions about using the framework or third-party bundles,
+    please email us contact AT spomky-labs.com for quoting
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 1beb0a38bf47c49a64411a94f142854476051755..029817b6263c90e8cae54c264ed9b581dc7695be 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,19 +1,17 @@
-| Q             | A
-| ------------- | ---
-| Branch?       | <!-- see below -->
-| Bug fix?      | yes/no
-| New feature?  | yes/no <!-- please update src/**/CHANGELOG.md files -->
-| Deprecations? | yes/no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
-| Tickets       | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
-| License       | MIT
+Target branch: 
+Resolves issue # <!-- #-prefixed issue number(s), if any -->
+
+<!-- replace space with "x" in square brackets: [x] -->
+- [ ] It is a Bug fix
+- [ ] It is a New feature
+- [ ] Breaks BC
+- [ ] Includes Deprecations
+
 <!--
-Replace this notice by a short README for your feature/bugfix. This will help people
-understand your PR and can be used as a start for the documentation.
+Fill in this template according to the PR you're about to submit.
+Replace this comment by a description of what your PR is solving.
 
-Additionally:
- - Always add tests and ensure they pass.
- - Never break backward compatibility (unless you are working on the next major release branch).
- - Bug fixes must be submitted against the lowest maintained branch where they apply
-   (lowest branches are regularly merged to upper ones so they get the fixes too.)
- - Features and deprecations must be submitted against the last major branch (e.g. 1.x).
--->
\ No newline at end of file
+Please consider the following requirement:
+* Modification of existing tests should be avoided unless deemed necessary.
+* You MUST never open a PR related to a security issue. Contact Spomky in private at https://gitter.im/Spomky/
+-->
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 75436e42b0ab812b010b6dd6900713672891f104..ae2faa417ae57237adbb29eb1eb53cb8ef000336 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,8 +1,19 @@
 version: 2
 updates:
-- package-ecosystem: composer
-  directory: "/"
-  schedule:
-    interval: daily
-    time: "11:00"
-  open-pull-requests-limit: 10
+  - package-ecosystem: "composer"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+      day: "friday"
+    versioning-strategy: "widen"
+    open-pull-requests-limit: 20
+    allow:
+      - dependency-type: all
+    labels: [ "Dependencies" ]
+
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "monthly"
+    open-pull-requests-limit: 20
+    labels: [ "Dependencies" ]
diff --git a/.github/stale.yml b/.github/stale.yml
index 98284be67bc99e9f9ca7af7ac4fc2de3790f88dd..19367a6a66f45bdb6ab276746ba86ad6c24b1c70 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -1,8 +1,8 @@
-daysUntilStale: 60
+daysUntilStale: 30
 daysUntilClose: 7
 staleLabel: wontfix
 markComment: >
-  Ce problème a été automatiquement marqué comme périmé car il n'a pas eu
-  d’activité récente. Il sera fermé dans 7 jours si aucune autre activité ne se produit. Merci
-  pour votre contribution.
+  This issue has been automatically marked as stale because it has not had
+  recent activity. It will be closed if no further activity occurs. Thank you
+  for your contributions.
 closeComment: false
diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml
deleted file mode 100644
index 584b524ce46cc9e56a98ab466981028fd1b56fa4..0000000000000000000000000000000000000000
--- a/.github/workflows/coding-standards.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-name: Coding Standards
-
-on: [push]
-
-jobs:
-  tests:
-    runs-on: ${{ matrix.operating-system }}
-    strategy:
-      matrix:
-        operating-system: [ubuntu-latest]
-        php-versions: ['8.1']
-    name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }}
-
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v2
-        with:
-          ref: ${{ github.head_ref }}
-
-      - name: Setup PHP, with composer and extensions
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: ${{ matrix.php-versions }}
-          extensions: mbstring
-          coverage: xdebug
-
-      - name: Install the application
-        run: |
-          composer install --no-progress --prefer-dist --optimize-autoloader
-
-      - name: Coding Standards Checks
-        run: make ci-cs
diff --git a/.github/workflows/gitsplit.yml b/.github/workflows/gitsplit.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0677186146e18ffb9945395f32bef380013f184f
--- /dev/null
+++ b/.github/workflows/gitsplit.yml
@@ -0,0 +1,18 @@
+name: gitsplit
+on:
+  push:
+    tags:
+      - '*'
+  release:
+    types: [ published ]
+
+jobs:
+  gitsplit:
+    runs-on: ubuntu-latest
+    steps:
+      - name: checkout
+        run: git clone https://github.com/web-auth/webauthn-framework /home/runner/work/web-auth/webauthn-framework && cd /home/runner/work/web-auth/webauthn-framework
+      - name: Split repositories
+        run: docker run --rm -t -e GH_TOKEN -v /cache/gitsplit:/cache/gitsplit -v /home/runner/work/web-auth/webauthn-framework:/srv jderusse/gitsplit gitsplit
+        env:
+          GH_TOKEN: ${{ secrets.GITSPLIT_TOKEN }}
diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5578d067d87b6f454244bb516fde95d3eaf7137c
--- /dev/null
+++ b/.github/workflows/integrate.yml
@@ -0,0 +1,230 @@
+# yaml-language-server: $schema=https://json.schemastore.org/github-workflow
+
+name: "Integrate"
+
+on:
+  push:
+    branches:
+      - "*.x"
+  pull_request: null
+
+jobs:
+  byte_level:
+    name: "0️⃣ Byte-level"
+    runs-on: "ubuntu-latest"
+    steps:
+      - name: "Checkout code"
+        uses: "actions/checkout@v3"
+
+      - name: "Check file permissions"
+        run: |
+          test "$(find . -type f -not -path './.git/*' -executable)" == ""
+
+      - name: "Find non-printable ASCII characters"
+        run: |
+          ! LC_ALL=C.UTF-8 find . -type f -name "*.php" -print0 | xargs -0 -- grep -PHn "[^ -~]"
+
+  syntax_errors:
+    name: "1️⃣ Syntax errors"
+    runs-on: "ubuntu-latest"
+    steps:
+      - name: "Set up PHP"
+        uses: "shivammathur/setup-php@v2"
+        with:
+          php-version: "8.1"
+          coverage: "none"
+
+      - name: "Checkout code"
+        uses: "actions/checkout@v3"
+
+      - name: "Install dependencies"
+        uses: "ramsey/composer-install@v2"
+        with:
+          dependency-versions: "highest"
+
+      - name: "Check source code for syntax errors"
+        run: "composer exec -- parallel-lint src/ tests/"
+
+  unit_tests:
+    name: "2️⃣ Unit and functional tests"
+    needs:
+      - "byte_level"
+      - "syntax_errors"
+    strategy:
+      matrix:
+        operating-system:
+          - "ubuntu-latest"
+        php-version:
+          - "8.1"
+        dependencies:
+          - "lowest"
+          - "highest"
+    runs-on: ${{ matrix.operating-system }}
+    steps:
+      - name: "Set up PHP"
+        uses: "shivammathur/setup-php@v2"
+        with:
+          php-version: "${{ matrix.php-version }}"
+          extensions: "mbstring"
+          coverage: "xdebug"
+
+      - name: "Checkout code"
+        uses: "actions/checkout@v3"
+
+      - name: "Install dependencies"
+        uses: "ramsey/composer-install@v2"
+        with:
+          dependency-versions: "${{ matrix.dependencies }}"
+          composer-options: "--optimize-autoloader"
+
+      - name: "Execute tests (PHP)"
+        run: "make ci-cc"
+
+  #      - name: Send coverage to Coveralls
+  #        if: "matrix.php-version == '8.1' && matrix.dependencies == 'highest'"
+  #        env:
+  #          COVERALLS_REPO_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+  #        run: |
+  #          wget "https://github.com/php-coveralls/php-coveralls/releases/download/v2.5.2/php-coveralls.phar"
+  #          php ./php-coveralls.phar -v
+
+  static_analysis:
+    name: "3️⃣ Static Analysis"
+    needs:
+      - "byte_level"
+      - "syntax_errors"
+    runs-on: "ubuntu-latest"
+    steps:
+      - name: "Set up PHP"
+        uses: "shivammathur/setup-php@v2"
+        with:
+          php-version: "8.1"
+          extensions: "mbstring"
+          coverage: "none"
+
+      - name: "Checkout code"
+        uses: "actions/checkout@v3"
+
+      - name: "Validate Composer configuration"
+        run: "composer validate --strict"
+
+      - name: "Install dependencies"
+        uses: "ramsey/composer-install@v2"
+        with:
+          dependency-versions: "highest"
+          composer-options: "--optimize-autoloader"
+
+      - name: "Check PSR-4 mapping"
+        run: "composer dump-autoload --optimize --strict-psr"
+
+      - name: "Execute static analysis"
+        run: "make st"
+
+  coding_standards:
+    name: "4️⃣ Coding Standards"
+    needs:
+      - "byte_level"
+      - "syntax_errors"
+    runs-on: "ubuntu-latest"
+    steps:
+      - name: "Set up PHP"
+        uses: "shivammathur/setup-php@v2"
+        with:
+          php-version: "8.1"
+          extensions: "mbstring"
+          coverage: "none"
+
+      - name: "Checkout code"
+        uses: "actions/checkout@v3"
+
+      - name: "Check adherence to EditorConfig"
+        uses: "greut/eclint-action@v0"
+
+      - name: "Install dependencies"
+        uses: "ramsey/composer-install@v2"
+        with:
+          dependency-versions: "highest"
+          composer-options: "--optimize-autoloader"
+
+      - name: "Check coding style"
+        run: "make ci-cs"
+
+      - name: "Deptrac"
+        run: |
+          vendor/bin/deptrac analyse --fail-on-uncovered --no-cache
+
+  mutation_testing:
+    name: "5️⃣ Mutation Testing"
+    needs:
+      - "byte_level"
+      - "syntax_errors"
+    runs-on: "ubuntu-latest"
+    steps:
+      - name: "Set up PHP"
+        uses: "shivammathur/setup-php@v2"
+        with:
+          php-version: "8.1"
+          extensions: "mbstring"
+          coverage: "xdebug"
+
+      - name: "Checkout code"
+        uses: "actions/checkout@v3"
+
+      - name: "Fetch Git base reference"
+        run: "git fetch --depth=1 origin ${GITHUB_BASE_REF}"
+
+      - name: "Install dependencies"
+        uses: "ramsey/composer-install@v2"
+        with:
+          dependency-versions: "highest"
+          composer-options: "--optimize-autoloader"
+
+      - name: "Execute Infection"
+        run: "make ci-mu"
+
+  rector_checkstyle:
+    name: "6️⃣ Rector Checkstyle"
+    needs:
+      - "byte_level"
+      - "syntax_errors"
+    runs-on: "ubuntu-latest"
+    steps:
+      - name: "Set up PHP"
+        uses: "shivammathur/setup-php@v2"
+        with:
+          php-version: "8.1"
+          extensions: "mbstring"
+          coverage: "xdebug"
+
+      - name: "Checkout code"
+        uses: "actions/checkout@v3"
+
+      - name: "Fetch Git base reference"
+        run: "git fetch --depth=1 origin ${GITHUB_BASE_REF}"
+
+      - name: "Install dependencies"
+        uses: "ramsey/composer-install@v2"
+        with:
+          dependency-versions: "highest"
+          composer-options: "--optimize-autoloader"
+
+      - name: "Execute Rector"
+        run: "make rector"
+
+  exported_files:
+    name: "7️⃣ Exported files"
+    needs:
+      - "byte_level"
+      - "syntax_errors"
+    runs-on: "ubuntu-20.04"
+    steps:
+      - name: "Checkout code"
+        uses: "actions/checkout@v3"
+
+      - name: "Check exported files"
+        run: |
+          EXPECTED="LICENSE,README.md,SECURITY.md,composer.json"
+          CURRENT="$(git archive HEAD | tar --list --exclude="src" --exclude="src/*" | paste -s -d ",")"
+          echo "CURRENT =${CURRENT}"
+          echo "EXPECTED=${EXPECTED}"
+          test "${CURRENT}" == "${EXPECTED}"
diff --git a/.github/workflows/merge-me.yml b/.github/workflows/merge-me.yml
new file mode 100644
index 0000000000000000000000000000000000000000..77e8f7d83cc29957995a5d829508963a61af19e7
--- /dev/null
+++ b/.github/workflows/merge-me.yml
@@ -0,0 +1,28 @@
+name: Merge me!
+
+on:
+  check_suite:
+    types:
+      - completed
+
+jobs:
+  merge-me:
+    name: Merge me!
+    runs-on: ubuntu-latest
+    steps:
+      - name: Merge me!
+        uses: ridedott/merge-me-action@v2.10.19
+        with:
+          # Depending on branch protection rules, a  manually populated
+          # `GITHUB_TOKEN_WORKAROUND` environment variable with permissions to
+          # push to a protected branch must be used. This variable can have an
+          # arbitrary name, as an example, this repository uses
+          # `GITHUB_TOKEN_DOTTBOTT`.
+          #
+          # When using a custom token, it is recommended to leave the following
+          # comment for other developers to be aware of the reasoning behind it:
+          #
+          # This must be used as GitHub Actions token does not support
+          # pushing to protected branches.
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          MERGE_METHOD: MERGE
diff --git a/.github/workflows/mutation-tests.yml b/.github/workflows/mutation-tests.yml
deleted file mode 100644
index 2277a9821695e58b1b6aee7a8c6316725d335bdd..0000000000000000000000000000000000000000
--- a/.github/workflows/mutation-tests.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: Mutation Testing
-
-on: [push]
-
-jobs:
-  tests:
-    runs-on: ${{ matrix.operating-system }}
-    strategy:
-      matrix:
-        operating-system: [ubuntu-latest]
-        php-versions: ['8.1']
-    name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }}
-
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v2
-        with:
-          ref: ${{ github.head_ref }}
-
-      - name: Setup PHP, with composer and extensions
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: ${{ matrix.php-versions }}
-          extensions: mbstring
-          coverage: xdebug
-
-      - name: Install the application
-        run: |
-          composer install --no-progress --prefer-dist --optimize-autoloader
-
-      - name: Fetch Git base reference
-        run: git fetch --depth=1 origin $GITHUB_BASE_REF
-
-      - name: Infection
-        run: make ci-mu
diff --git a/.github/workflows/rector_checkstyle.yaml b/.github/workflows/rector_checkstyle.yaml
deleted file mode 100644
index 53f8d36d08fb46b719bead12c8902ca0fd68b9d8..0000000000000000000000000000000000000000
--- a/.github/workflows/rector_checkstyle.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: Rector Checkstyle
-
-on: [push]
-
-jobs:
-  tests:
-    runs-on: ${{ matrix.operating-system }}
-    strategy:
-      matrix:
-        operating-system: [ ubuntu-latest ]
-        php-versions: ['8.1']
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v2
-        with:
-          ref: ${{ github.head_ref }}
-
-      - name: Setup PHP, with composer and extensions
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: ${{ matrix.php-versions }}
-          extensions: mbstring
-          coverage: none
-
-      - name: Install the application
-        run: |
-          composer install --no-progress --prefer-dist --optimize-autoloader
-
-      - name: Rector
-        run: make ci-rector
diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml
new file mode 100644
index 0000000000000000000000000000000000000000..babdc0922c9f4fe1dfb6a8db7a34eaa2e2af3cb2
--- /dev/null
+++ b/.github/workflows/release-on-milestone-closed.yml
@@ -0,0 +1,72 @@
+# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
+
+name: "Automatic Releases"
+
+on:
+  milestone:
+    types:
+      - "closed"
+
+jobs:
+  release:
+    name: "GIT tag, release & create merge-up PR"
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: "Checkout"
+        uses: "actions/checkout@v3"
+
+      - name: "Release"
+        uses: "laminas/automatic-releases@1.17.0"
+        with:
+          command-name: "laminas:automatic-releases:release"
+        env:
+          "SHELL_VERBOSITY": "3"
+          "GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
+          "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
+          "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
+          "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
+
+      - name: "Create Merge-Up Pull Request"
+        uses: "laminas/automatic-releases@1.17.0"
+        with:
+          command-name: "laminas:automatic-releases:create-merge-up-pull-request"
+        env:
+          "SHELL_VERBOSITY": "3"
+          "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
+          "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
+          "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
+          "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
+
+      - name: "Create and/or Switch to new Release Branch"
+        uses: "laminas/automatic-releases@1.17.0"
+        with:
+          command-name: "laminas:automatic-releases:switch-default-branch-to-next-minor"
+        env:
+          "SHELL_VERBOSITY": "3"
+          "GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
+          "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
+          "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
+          "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
+
+      - name: "Bump Changelog Version On Originating Release Branch"
+        uses: "laminas/automatic-releases@1.17.0"
+        with:
+          command-name: "laminas:automatic-releases:bump-changelog"
+        env:
+          "SHELL_VERBOSITY": "3"
+          "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
+          "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
+          "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
+          "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
+
+      - name: "Create new milestones"
+        uses: "laminas/automatic-releases@1.17.0"
+        with:
+          command-name: "laminas:automatic-releases:create-milestones"
+        env:
+          "SHELL_VERBOSITY": "3"
+          "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
+          "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
+          "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
+          "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
diff --git a/.github/workflows/static-analyze.yml b/.github/workflows/static-analyze.yml
deleted file mode 100644
index 91154b0e13913726c515274becbcb619c61a1bf9..0000000000000000000000000000000000000000
--- a/.github/workflows/static-analyze.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: Static Analyze
-
-on: [push]
-
-jobs:
-  tests:
-    runs-on: ${{ matrix.operating-system }}
-    strategy:
-      matrix:
-        operating-system: [ubuntu-latest]
-        php-versions: ['8.1']
-    name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }}
-
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v2
-        with:
-          ref: ${{ github.head_ref }}
-
-      - name: Setup PHP, with composer and extensions
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: ${{ matrix.php-versions }}
-          extensions: mbstring
-          coverage: xdebug
-          tools: cs2pr
-
-      - name: Install the application
-        run: |
-          composer install --no-progress --prefer-dist --optimize-autoloader
-
-      - name: Static Analyze Checks
-        run: make ci-st
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
index 71f18a43e684c2c355968d7db0fae5a3e93a3a72..0000000000000000000000000000000000000000
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-name: Unit and Functional Tests
-
-on: [push]
-
-jobs:
-  tests:
-    runs-on: ${{ matrix.operating-system }}
-    strategy:
-      matrix:
-        operating-system: [ ubuntu-latest ]
-        php-versions: ['8.1']
-    name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }}
-
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v2
-        with:
-          ref: ${{ github.head_ref }}
-
-      - name: Setup PHP, with composer and extensions
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: ${{ matrix.php-versions }}
-          extensions: mbstring
-          coverage: xdebug
-
-      - name: Install the application
-        run: |
-          composer install --no-progress --prefer-dist --optimize-autoloader
-
-      - name: Run tests
-        run: make all
diff --git a/.github/workflows/tweet.yml b/.github/workflows/tweet.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5f9acda82cd397e7de43686a03098f9aad8c1497
--- /dev/null
+++ b/.github/workflows/tweet.yml
@@ -0,0 +1,23 @@
+name: tweet
+on:
+  push:
+    tags:
+      - '*'
+  release:
+    types: [ published ]
+
+jobs:
+  tweet:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Tweet
+        uses: snow-actions/tweet@v1.3.0
+        with:
+          status: |
+            We are proud to announce that ${{ github.repository }} · ${{ github.event.release.name }}
+            ${{ github.event.release.html_url }} is now released 🚀. #php #totp #hotp
+        env:
+          CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
+          CONSUMER_API_SECRET_KEY: ${{ secrets.TWITTER_CONSUMER_API_SECRET }}
+          ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
+          ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
diff --git a/.gitignore b/.gitignore
index 9c4be60124746b4808e407cdd1c266e02c05bad0..757b91777eaaac28819f56de5cef5409b02f04e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,4 @@
 /vendor/
 .phpunit.result.cache
 /phpunit.xml
-/infection.log
\ No newline at end of file
+/infection.log
diff --git a/Makefile b/Makefile
index 529b80e141bd5e11bca40138e60cab25f6600b9c..159530a0bd3f58d94b32ef052b65be165a7d55fb 100644
--- a/Makefile
+++ b/Makefile
@@ -1,63 +1,69 @@
 ########################
-#         CI/CD        #
+#      Everyday        #
 ########################
 
-ci-cs: vendor ## Check all files using defined rules (CI/CD)
-	vendor/bin/ecs check
+.PHONY: mu
+mu: vendor ## Mutation tests
+	vendor/bin/infection -s --threads=$$(nproc) --min-msi=30 --min-covered-msi=50
 
-ci-st: vendor ## Run static analyse (CI/CD)
-	vendor/bin/phpstan analyse --error-format=checkstyle | cs2pr
+.PHONY: tests
+tests: vendor ## Run all tests
+	vendor/bin/phpunit  --color
+	yarn test
 
-ci-rector: vendor ## Check all files using Rector (CI/CD)
-	vendor/bin/rector process --ansi --dry-run
+.PHONY: cc
+cc: vendor ## Show test coverage rates (HTML)
+	vendor/bin/phpunit --coverage-html ./build
 
-ci-mu: vendor ## Mutation tests (CI/CD)
-	vendor/bin/infection --logger-github -s --threads=$$(nproc) --min-msi=70 --min-covered-msi=50 --test-framework-options="--exclude-group=Performance"
+.PHONY: cs
+cs: vendor ## Fix all files using defined ECS rules
+	vendor/bin/ecs check --fix
 
-########################
-#      Everyday        #
-########################
+.PHONY: tu
+tu: vendor ## Run only unit tests
+	vendor/bin/phpunit --color --group Unit
 
-all: vendor ## Run all tests
-	vendor/bin/phpunit --color
+.PHONY: ti
+ti: vendor ## Run only integration tests
+	vendor/bin/phpunit --color --group Integration
 
-tu: vendor ## Run only unit tests
-	vendor/bin/phpunit --color tests
+.PHONY: tf
+tf: vendor ## Run only functional tests
+	vendor/bin/phpunit --color --group Functional
 
+.PHONY: st
 st: vendor ## Run static analyse
-	vendor/bin/phpstan analyse
+	XDEBUG_MODE=off vendor/bin/phpstan analyse
 
 
 ########################
-#      Every PR        #
+#         CI/CD        #
 ########################
 
-cs: vendor ## Fix all files using defined rules
-	vendor/bin/ecs check --fix
+.PHONY: ci-mu
+ci-mu: vendor ## Mutation tests (for CI/CD only)
+	vendor/bin/infection --logger-github -s --threads=$$(nproc) --min-msi=30 --min-covered-msi=50
 
-rector: vendor ## Check all files using Rector
-	vendor/bin/rector process
+.PHONY: ci-cc
+ci-cc: vendor ## Show test coverage rates (for CI/CD only)
+	vendor/bin/phpunit --coverage-text
 
+.PHONY: ci-cs
+ci-cs: vendor ## Check all files using defined ECS rules (for CI/CD only)
+	XDEBUG_MODE=off vendor/bin/ecs check
 
 ########################
 #        Others        #
 ########################
 
-mu: vendor ## Mutation tests
-	vendor/bin/infection -s --threads=$$(nproc) --min-msi=70 --min-covered-msi=50 --test-framework-options="--exclude-group=Performance"
-
-cc: vendor ## Show test coverage rates (HTML)
-	vendor/bin/phpunit --coverage-html ./build
+.PHONY: rector
+rector: vendor ## Check all files using Rector
+	XDEBUG_MODE=off vendor/bin/rector process --ansi --dry-run --xdebug
 
-vendor: composer.json composer.lock
+vendor: composer.json
 	composer validate
 	composer install
 
-
-########################
-#      Default         #
-########################
-
 .DEFAULT_GOAL := help
 help:
 	@grep -E '(^[a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/'
diff --git a/composer.json b/composer.json
index bff24fa1580d2ee1036c4afe2b257035724fd701..30db9729dd16b2088a07a8a31b632e64ea687f30 100644
--- a/composer.json
+++ b/composer.json
@@ -23,13 +23,15 @@
     "require-dev": {
         "ekino/phpstan-banned-code": "^1.0",
         "infection/infection": "^0.26",
+        "php-parallel-lint/php-parallel-lint": "^1.3",
         "phpstan/phpstan": "^1.0",
         "phpstan/phpstan-deprecation-rules": "^1.0",
         "phpstan/phpstan-phpunit": "^1.0",
         "phpstan/phpstan-strict-rules": "^1.0",
-        "phpunit/phpunit": "^9.5",
+        "phpunit/phpunit": "^9.5.26",
+        "qossmic/deptrac-shim": "^1.0",
         "rector/rector": "^0.14",
-        "symfony/phpunit-bridge": "^6.0",
+        "symfony/phpunit-bridge": "^6.1",
         "symplify/easy-coding-standard": "^11.0"
     },
     "autoload": {
diff --git a/deptrac.yaml b/deptrac.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1986e15e430b48e69667274a745acd43b1ed447f
--- /dev/null
+++ b/deptrac.yaml
@@ -0,0 +1,14 @@
+parameters:
+  paths:
+    - './src'
+  layers:
+    - name: 'OTP'
+      collectors:
+        - type: 'directory'
+          regex: 'src/.*'
+    - name: 'Vendors'
+      collectors:
+        - { type: className, regex: '^ParagonIE\\' }
+  ruleset:
+    OTP:
+      - 'Vendors'
diff --git a/infection.json.dist b/infection.json.dist
index f02cbf8717b764e2f6f7a8a46b034e03c49218e8..00a3453d7b751e74f0ec0907420531174fd75874 100644
--- a/infection.json.dist
+++ b/infection.json.dist
@@ -9,8 +9,11 @@
     },
     "mutators": {
         "@default": true,
-        "global-ignoreSourceCodeByRegex": [
-           "\\$this->logger.*"
-        ]
+        "MBString": {
+            "settings": {
+                "mb_substr": false,
+                "mb_strlen": false
+            }
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/phpstan.neon b/phpstan.neon
index e55b63567665aea4c8f07671e5507d964f764804..2ce3a2c42ff3808e186faab575b7e97f7e418eef 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -4,10 +4,25 @@ parameters:
         - src
         - tests
     ignoreErrors:
-        - '#Variable property access on \$this\(OTPHP\\OTP\)\.#'
-        - '#^Method OTPHP\\OTP::generateSecret\(\) should return non-empty-string but returns string\.$#'
+        -
+            message: '#Variable property access on \$this\(OTPHP\\OTP\)\.#'
+            path: src/ParameterTrait.php
+            count: 1
+        -
+            message: '#^Method OTPHP\\OTP::generateSecret\(\) should return non-empty-string but returns string\.$#'
+            path: src/OTP.php
+            count: 1
+        -
+            message: '#^Cannot cast mixed to int\.$#'
+            path: src/HOTP.php
+            count: 1
+        -
+            message: '#^Parameter \#\d .* of class OTPHP\\Url constructor expects .*\, .* given\.$#'
+            path: src/Url.php
+            count: 2
 
 includes:
+    - vendor/phpstan/phpstan/conf/bleedingEdge.neon
     - vendor/phpstan/phpstan-strict-rules/rules.neon
     - vendor/phpstan/phpstan-phpunit/extension.neon
     - vendor/phpstan/phpstan-deprecation-rules/rules.neon
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index d4bd675ced25b45caddb42640b5d2e4ce31fae43..e52c02c0fbba2ff97270ecb0e5d37dae80dcd4b4 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,24 +1,28 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
-  <coverage>
-    <include>
-      <directory suffix=".php">./src</directory>
-    </include>
-  </coverage>
-  <testsuites>
-    <testsuite name="OTP Test Suite">
-      <directory>./tests</directory>
-    </testsuite>
-  </testsuites>
-  <listeners>
-    <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
-      <arguments>
-        <array>
-          <element key="time-sensitive"><string>OTPHP</string></element>
-          <element key="time-sensitive"><string>OTPHP\TOTP</string></element>
-        </array>
-      </arguments>
-    </listener>
-    <listener class="Symfony\Bridge\PhpUnit\CoverageListener" />
-  </listeners>
+<phpunit
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    bootstrap="vendor/autoload.php"
+    colors="true"
+    resolveDependencies="true"
+    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
+>
+    <coverage>
+        <include>
+            <directory suffix=".php">./src</directory>
+        </include>
+    </coverage>
+    <testsuites>
+        <testsuite name="OTP Test Suite">
+            <directory>./tests</directory>
+        </testsuite>
+    </testsuites>
+    <listeners>
+        <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
+            <arguments>
+                <array>
+                    <element key="time-sensitive"><string>OTPHP\TOTP</string></element>
+                </array>
+            </arguments>
+        </listener>
+    </listeners>
 </phpunit>
diff --git a/src/HOTP.php b/src/HOTP.php
index 8dc18d3ef5421bb55af7f6c66e6988401220ffad..aa5a2275417689e359cc53eeed449c274df72d9f 100644
--- a/src/HOTP.php
+++ b/src/HOTP.php
@@ -12,6 +12,8 @@ use function is_int;
  */
 final class HOTP extends OTP implements HOTPInterface
 {
+    private const DEFAULT_WINDOW = 0;
+
     public static function create(
         null|string $secret = null,
         int $counter = self::DEFAULT_COUNTER,
@@ -86,10 +88,11 @@ final class HOTP extends OTP implements HOTPInterface
     protected function getParameterMap(): array
     {
         return [...parent::getParameterMap(), ...[
-            'counter' => static function ($value): int {
-                (int) $value >= 0 || throw new InvalidArgumentException('Counter must be at least 0.');
+            'counter' => static function (mixed $value): int {
+                $value = (int) $value;
+                $value >= 0 || throw new InvalidArgumentException('Counter must be at least 0.');
 
-                return (int) $value;
+                return $value;
             },
         ]];
     }
@@ -101,7 +104,7 @@ final class HOTP extends OTP implements HOTPInterface
 
     private function getWindow(null|int $window): int
     {
-        return abs($window ?? 0);
+        return abs($window ?? self::DEFAULT_WINDOW);
     }
 
     private function verifyOtpWithWindow(string $otp, int $counter, null|int $window): bool
diff --git a/src/OTP.php b/src/OTP.php
index ba640a3d2ea3e66b7f5b698f2a871d2d500ceb71..2cba067b3cd73f01b6c2d75e1b7d672dc0473aab 100644
--- a/src/OTP.php
+++ b/src/OTP.php
@@ -17,6 +17,8 @@ abstract class OTP implements OTPInterface
 {
     use ParameterTrait;
 
+    private const DEFAULT_SECRET_SIZE = 64;
+
     /**
      * @param non-empty-string $secret
      */
@@ -42,7 +44,7 @@ abstract class OTP implements OTPInterface
      */
     final protected static function generateSecret(): string
     {
-        return Base32::encodeUpper(random_bytes(64));
+        return Base32::encodeUpper(random_bytes(self::DEFAULT_SECRET_SIZE));
     }
 
     /**
diff --git a/src/ParameterTrait.php b/src/ParameterTrait.php
index 80d38888325ff7f09c97cceacd28f6d0e6d93f13..b05092351b02fc162e3045cc79b98d044ec598ed 100644
--- a/src/ParameterTrait.php
+++ b/src/ParameterTrait.php
@@ -149,9 +149,7 @@ trait ParameterTrait
 
                 return $value;
             },
-            'secret' => static function ($value): string {
-                return mb_strtoupper(trim($value, '='));
-            },
+            'secret' => static fn ($value): string => mb_strtoupper(trim((string) $value, '=')),
             'algorithm' => static function ($value): string {
                 $value = mb_strtolower($value);
                 in_array($value, hash_algos(), true) || throw new InvalidArgumentException(sprintf(
diff --git a/src/Url.php b/src/Url.php
index 387d54fa5d5c3da89d0d827280ce5d554047d077..56ad979c5b6b48e7c496d4ef72d9de5caaaaa273 100644
--- a/src/Url.php
+++ b/src/Url.php
@@ -15,13 +15,13 @@ final class Url
 {
     /**
      * @param non-empty-string $secret
+     * @param array<string, mixed> $query
      */
     public function __construct(
         private readonly string $scheme,
         private readonly string $host,
         private readonly string $path,
         private readonly string $secret,
-        /** @var array<string, mixed> $query */
         private readonly array $query
     ) {
     }
diff --git a/tests/TOTPTest.php b/tests/TOTPTest.php
index 03691da92b97e956bdec01bb42c0fbb5ca89e5e0..53d7284675d1fca5b66462eb41a40ad79393d772 100644
--- a/tests/TOTPTest.php
+++ b/tests/TOTPTest.php
@@ -164,9 +164,12 @@ final class TOTPTest extends TestCase
      */
     public function generateOtpNow(): void
     {
+        ClockMock::register(TOTP::class);
+        $time = time();
+        ClockMock::withClockMock($time);
         $otp = $this->createTOTP(6, 'sha1', 30);
 
-        static::assertSame($otp->now(), $otp->at(time()));
+        static::assertSame($otp->now(), $otp->at($time));
     }
 
     /**
@@ -174,7 +177,9 @@ final class TOTPTest extends TestCase
      */
     public function verifyOtpNow(): void
     {
+        ClockMock::register(TOTP::class);
         $time = time();
+        ClockMock::withClockMock($time);
         $otp = $this->createTOTP(6, 'sha1', 30);
 
         $totp = $otp->at($time);
@@ -308,8 +313,12 @@ final class TOTPTest extends TestCase
      * @test
      * @dataProvider dataLeewayWithEpoch
      */
-    public function verifyOtpWithEpochInWindow(int $timestamp, string $input, int $leeway, bool $expectedResult): void
-    {
+    public function verifyOtpWithEpochInWindow(
+        int $timestamp,
+        string $input,
+        int $leeway,
+        bool $expectedResult
+    ): void {
         ClockMock::register(TOTP::class);
         ClockMock::withClockMock($timestamp);
         $otp = $this->createTOTP(6, 'sha1', 30, 'JDDK4U6G3BJLEZ7Y', 'alice@foo.bar', 'My Project', 100);