Compare commits
105 Commits
v34.7.0-rc.0
...
v34.13.0
| Author | SHA1 | Date | |
|---|---|---|---|
| d3f5526ec0 | |||
| 13df897896 | |||
| 405a6fbb92 | |||
| d6f3262e12 | |||
| 8b32f3eb7f | |||
| 1e9934a69d | |||
| a6d342d3a4 | |||
| 4b9a1bd53f | |||
| 26248f85d5 | |||
| ae2ae483db | |||
| c87692d0aa | |||
| 2dd4334e20 | |||
| 2210255d6f | |||
| 544ac86d20 | |||
| cf06547063 | |||
| 781c3b05e5 | |||
| 325dace437 | |||
| 5c894b34b3 | |||
| f5f4091a00 | |||
| 52bdb57a47 | |||
| 3c23eb69b5 | |||
| c93b7ce188 | |||
| 705b6336cf | |||
| 7c41e3fb06 | |||
| a314e612fa | |||
| ac6cad2852 | |||
| 906390f0bf | |||
| 795497fafa | |||
| ffb777d118 | |||
| fbba8a2d71 | |||
| 053c5741b0 | |||
| 620dc2f6e2 | |||
| 00d78077b0 | |||
| 23d2d03912 | |||
| b4c4355d1a | |||
| 142c0a65e6 | |||
| b9aacea1cb | |||
| 76e653b7ee | |||
| 4d4ff4c3f2 | |||
| 00aba742e4 | |||
| 35d862ebd3 | |||
| 581b3209ab | |||
| 6855ace642 | |||
| 5033d48013 | |||
| 4c53836a13 | |||
| 10a4fd8328 | |||
| 98f7637683 | |||
| 0df8e81da4 | |||
| 30b1894f37 | |||
| fbbdb6e766 | |||
| 5a1488ebd5 | |||
| c4048d985d | |||
| 794f044dda | |||
| a197afe8aa | |||
| 1061b93b29 | |||
| 6528a59fc1 | |||
| f6a169b5a5 | |||
| d04135cc1c | |||
| 546047a050 | |||
| 16153e5d82 | |||
| fd73d5068c | |||
| e72859a44a | |||
| a8f8a9c14d | |||
| 0e2f73d7a7 | |||
| 31aeb3044f | |||
| 0a29063bc9 | |||
| 3cc3bd0728 | |||
| f891fe4423 | |||
| b99ff83785 | |||
| 23c4c9fd8a | |||
| 8b8ee91210 | |||
| d8c431f23e | |||
| a6fb7530cb | |||
| b5c3f15a67 | |||
| 91f6f0f9c5 | |||
| 88cf5eb769 | |||
| 13a967ae8f | |||
| 66c80949e8 | |||
| 3d51e31da8 | |||
| 86c190fa0d | |||
| e9a9280e3c | |||
| fabdd6da64 | |||
| ab5d95102c | |||
| b4556d6552 | |||
| 6c22da9a96 | |||
| d29329bddb | |||
| 68b06d7b60 | |||
| 5f599ee978 | |||
| aabd558bce | |||
| 662b772c73 | |||
| b240c44128 | |||
| 5508993d79 | |||
| c9b43ab251 | |||
| 2fb1e659c8 | |||
| b842192a34 | |||
| 868bbfcb31 | |||
| 860161bdd2 | |||
| 0c9d82e40a | |||
| 73ab7c342a | |||
| 3386c66b98 | |||
| da044820d7 | |||
| 9f40f32b3e | |||
| f679942584 | |||
| 1dcb7a6e75 | |||
| 823316b2ff |
+8
-6
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "import", "jsdoc", "node"],
|
||||
plugins: ["matrix-org", "import", "jsdoc", "n"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
@@ -63,6 +63,10 @@ module.exports = {
|
||||
name: "setImmediate",
|
||||
message: "Use setTimeout instead.",
|
||||
},
|
||||
{
|
||||
name: "global",
|
||||
message: "Use globalThis instead.",
|
||||
},
|
||||
],
|
||||
|
||||
"import/no-restricted-paths": [
|
||||
@@ -108,10 +112,8 @@ module.exports = {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
// The non-TypeScript rule produces false positives
|
||||
"func-call-spacing": "off",
|
||||
"@typescript-eslint/func-call-spacing": ["error"],
|
||||
// We do this sometimes to brand interfaces
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
@@ -129,7 +131,7 @@ module.exports = {
|
||||
// "jsdoc/check-param-names": "error",
|
||||
// "jsdoc/check-indentation": "error",
|
||||
// Ensure .ts extension on imports outside of tests
|
||||
"node/file-extension-in-import": [
|
||||
"n/file-extension-in-import": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/matrixrtc @matrix-org/element-call-reviewers
|
||||
|
||||
/src/crypto-api @matrix-org/element-crypto-web-reviewers
|
||||
/src/crypto @matrix-org/element-crypto-web-reviewers
|
||||
/src/rust-crypto @matrix-org/element-crypto-web-reviewers
|
||||
/spec/integ/crypto @matrix-org/element-crypto-web-reviewers
|
||||
|
||||
@@ -22,7 +22,7 @@ runs:
|
||||
|
||||
- name: Upload tarball signature
|
||||
if: ${{ inputs.upload-url }}
|
||||
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
||||
|
||||
@@ -29,13 +29,13 @@ runs:
|
||||
|
||||
- name: Upload asset signatures
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}.asc
|
||||
|
||||
- name: Upload assets
|
||||
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}
|
||||
|
||||
@@ -7,10 +7,12 @@ on:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
|
||||
@@ -5,16 +5,19 @@ on:
|
||||
workflows: ["Static Analysis"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
netlify:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
actions: read
|
||||
deployments: write
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: docs
|
||||
path: docs
|
||||
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
#
|
||||
#push:
|
||||
# branches: [develop, master]
|
||||
|
||||
permissions: {} # No permissions required
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
@@ -21,14 +21,13 @@ concurrency:
|
||||
jobs:
|
||||
playwright:
|
||||
name: Playwright
|
||||
uses: element-hq/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
|
||||
uses: element-hq/element-web/.github/workflows/end-to-end-tests.yaml@develop
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: element-hq/matrix-react-sdk
|
||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume multiple concurrent runners.
|
||||
skip: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
@@ -3,6 +3,7 @@ on:
|
||||
push:
|
||||
branches: [develop]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
notify-downstream:
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
@@ -14,10 +15,8 @@ jobs:
|
||||
include:
|
||||
- repo: element-hq/element-web
|
||||
event: element-web-notify
|
||||
- repo: element-hq/matrix-react-sdk
|
||||
event: upstream-sdk-notify
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
||||
|
||||
@@ -9,10 +9,11 @@ on:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@d25134c992b943fb6ad00c25ea00eb5988c0a9dd # v5
|
||||
if: github.event_name != 'merge_group'
|
||||
@@ -29,7 +30,7 @@ jobs:
|
||||
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
@@ -42,7 +43,7 @@ jobs:
|
||||
|
||||
community-prs:
|
||||
name: Label Community PRs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
@@ -69,7 +70,7 @@ jobs:
|
||||
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
|
||||
@@ -8,9 +8,12 @@ on:
|
||||
type: string
|
||||
required: false
|
||||
concurrency: release-drafter-action
|
||||
permissions: {}
|
||||
jobs:
|
||||
draft:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
branches: [staging]
|
||||
workflow_dispatch: {}
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {}
|
||||
jobs:
|
||||
draft:
|
||||
permissions:
|
||||
contents: write
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
|
||||
|
||||
@@ -13,12 +13,14 @@ on:
|
||||
type: string
|
||||
required: false
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {} # Uses ELEMENT_BOT_TOKEN
|
||||
jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -49,9 +51,6 @@ jobs:
|
||||
git checkout develop
|
||||
git merge -X ours master
|
||||
|
||||
- name: Run post-merge-master script to revert package.json fields
|
||||
run: ./.action-repo/scripts/release/post-merge-master.sh
|
||||
|
||||
- name: Reset dependencies
|
||||
if: inputs.dependencies
|
||||
run: |
|
||||
@@ -61,26 +60,25 @@ jobs:
|
||||
CURRENT_VERSION=$(cat package.json | jq -r .dependencies[\"$PACKAGE\"])
|
||||
echo "Current $PACKAGE version is $CURRENT_VERSION"
|
||||
|
||||
if [ "$CURRENT_VERSION" == "null" ]
|
||||
if [[ "$CURRENT_VERSION" == "null" ]]
|
||||
then
|
||||
echo "Unable to find $PACKAGE in package.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_VERSION" == "develop" ]
|
||||
if [[ "$CURRENT_VERSION" == *"#develop" ]]
|
||||
then
|
||||
echo "Not updating dependency $PACKAGE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Resetting $1 to develop branch..."
|
||||
echo "Resetting $PACKAGE to develop branch..."
|
||||
yarn add "github:matrix-org/$PACKAGE#develop"
|
||||
git add -u
|
||||
git commit -m "Reset $PACKAGE back to develop branch"
|
||||
done <<< "$DEPENDENCIES"
|
||||
env:
|
||||
DEPENDENCIES: ${{ inputs.dependencies }}
|
||||
FINAL: ${{ inputs.final }}
|
||||
|
||||
- name: Push changes
|
||||
run: git push origin develop
|
||||
|
||||
@@ -38,16 +38,43 @@ on:
|
||||
description: The number of expected assets, including signatures, excluding generated zip & tarball.
|
||||
type: number
|
||||
required: false
|
||||
permissions: {}
|
||||
jobs:
|
||||
checks:
|
||||
name: Sanity checks
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Check for X-Release-Blocker label on any open issues or PRs
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data } = await github.rest.search.issuesAndPullRequests({
|
||||
q: `repo:${context.repo.owner}/${context.repo.repo} label:X-Release-Blocker is:open`,
|
||||
per_page: 50,
|
||||
});
|
||||
|
||||
if (data.total_count) {
|
||||
data.items.forEach(item => {
|
||||
core.error(`Release blocker: ${item.html_url}`);
|
||||
});
|
||||
core.setFailed(`Found release blockers!`);
|
||||
}
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
environment: Release
|
||||
needs: checks
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Load GPG key
|
||||
id: gpg
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6
|
||||
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
@@ -65,6 +92,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: staging
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -136,7 +164,9 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Bump package.json version
|
||||
run: yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
run: |
|
||||
yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
git add package.json
|
||||
|
||||
- name: Add to CHANGELOG.md
|
||||
if: inputs.final
|
||||
@@ -158,11 +188,6 @@ jobs:
|
||||
env:
|
||||
RELEASE_NOTES: ${{ steps.draft-release.outputs.body }}
|
||||
|
||||
- name: Run pre-release script to update package.json fields
|
||||
run: |
|
||||
./.action-repo/scripts/release/pre-release.sh
|
||||
git add package.json
|
||||
|
||||
- name: Commit changes
|
||||
run: git commit -m "$VERSION"
|
||||
|
||||
@@ -276,7 +301,9 @@ jobs:
|
||||
post-release:
|
||||
name: Post release steps
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- id: repository
|
||||
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||
@@ -304,7 +331,7 @@ jobs:
|
||||
bump-downstreams:
|
||||
name: Update npm dependency in downstream projects
|
||||
needs: npm
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: inputs.downstreams
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
@@ -8,10 +8,11 @@ on:
|
||||
id:
|
||||
description: "The npm package@version string we published"
|
||||
value: ${{ jobs.npm.outputs.id }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
id: ${{ steps.npm-publish.outputs.id }}
|
||||
steps:
|
||||
@@ -39,6 +40,10 @@ jobs:
|
||||
tag: next
|
||||
ignore-scripts: false
|
||||
|
||||
- name: Check npm package was published
|
||||
if: steps.npm-publish.outputs.id == ''
|
||||
run: exit 1
|
||||
|
||||
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
|
||||
run: npm dist-tag add "$release" latest
|
||||
|
||||
@@ -21,6 +21,7 @@ on:
|
||||
type: boolean
|
||||
default: true
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
release:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
|
||||
@@ -28,13 +29,13 @@ jobs:
|
||||
with:
|
||||
final: ${{ inputs.mode == 'final' }}
|
||||
npm: ${{ inputs.npm }}
|
||||
downstreams: '["element-hq/matrix-react-sdk", "element-hq/element-web"]'
|
||||
downstreams: '["element-hq/element-web"]'
|
||||
|
||||
docs:
|
||||
name: Publish Documentation
|
||||
needs: release
|
||||
if: inputs.docs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -60,7 +61,7 @@ jobs:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
needs: docs
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
|
||||
@@ -5,23 +5,29 @@ on:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
# No longer used
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
required: false
|
||||
inputs:
|
||||
sharded:
|
||||
type: boolean
|
||||
required: false
|
||||
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
|
||||
permissions: {}
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event != 'merge_group'
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
id-token: write # sonar
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
- uses: guibranco/github-status-action-v2@1f26a0237cd1a57626fbb5a0eb2494c9b8797d07
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -30,7 +36,7 @@ jobs:
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||
@@ -40,7 +46,7 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
if: ${{ !inputs.sharded }}
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: coverage
|
||||
path: coverage
|
||||
@@ -48,11 +54,17 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
if: inputs.sharded
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: coverage-*
|
||||
path: coverage
|
||||
merge-multiple: true
|
||||
- name: Check coverage artifact
|
||||
run: |
|
||||
if [ ! -d coverage ]; then
|
||||
echo "Coverage not found. Exiting with failure."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- id: extra_args
|
||||
run: |
|
||||
@@ -75,7 +87,7 @@ jobs:
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
- uses: guibranco/github-status-action-v2@1f26a0237cd1a57626fbb5a0eb2494c9b8797d07
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -7,10 +7,15 @@ on:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
permissions: {}
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
id-token: write # sonar
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
@@ -8,10 +8,11 @@ on:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
permissions: {} # No permissions needed
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -26,19 +27,9 @@ jobs:
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
- name: Switch js-sdk to release mode
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -55,7 +46,7 @@ jobs:
|
||||
|
||||
workflow_lint:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -72,7 +63,7 @@ jobs:
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -97,7 +88,7 @@ jobs:
|
||||
|
||||
analyse_dead_code:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -111,3 +102,37 @@ jobs:
|
||||
|
||||
- name: Run linter
|
||||
run: "yarn run lint:knip"
|
||||
|
||||
element-web:
|
||||
name: Downstream tsc element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: "./scripts/layered.sh"
|
||||
env:
|
||||
# tell layered.sh to check out the right sha of the JS-SDK
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
# Hook for branch protection to skip downstream typechecking outside of merge queues
|
||||
downstream:
|
||||
name: Downstream Typescript Syntax Check
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- element-web
|
||||
steps:
|
||||
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
- develop
|
||||
paths:
|
||||
- .github/labels.yml
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
sync-labels:
|
||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
|
||||
|
||||
+16
-10
@@ -10,10 +10,11 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
jest:
|
||||
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -68,15 +69,17 @@ jobs:
|
||||
name: Jest tests
|
||||
needs: jest
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
matrix-react-sdk:
|
||||
name: Downstream test matrix-react-sdk
|
||||
element-web:
|
||||
name: Downstream test element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: element-hq/matrix-react-sdk/.github/workflows/tests.yml@develop
|
||||
uses: element-hq/element-web/.github/workflows/tests.yml@develop
|
||||
permissions:
|
||||
statuses: write
|
||||
with:
|
||||
disable_coverage: true
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
@@ -84,6 +87,7 @@ jobs:
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
if: github.event_name == 'merge_group'
|
||||
permissions: read-all
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
|
||||
with:
|
||||
use_js_sdk: "."
|
||||
@@ -91,7 +95,7 @@ jobs:
|
||||
# we need this so the job is reported properly when run in a merge queue
|
||||
downstream-complement-crypto:
|
||||
name: Downstream Complement Crypto tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- complement-crypto
|
||||
@@ -103,14 +107,16 @@ jobs:
|
||||
# and skip sonarcloud coverage within merge queues
|
||||
downstream:
|
||||
name: Downstream tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- matrix-react-sdk
|
||||
- element-web
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- name: Skip SonarCloud on merge queues
|
||||
if: env.ENABLE_COVERAGE == 'false'
|
||||
uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
uses: guibranco/github-status-action-v2@1f26a0237cd1a57626fbb5a0eb2494c9b8797d07
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
@@ -119,5 +125,5 @@ jobs:
|
||||
sha: ${{ github.sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- if: needs.matrix-react-sdk.result != 'skipped' && needs.matrix-react-sdk.result != 'success'
|
||||
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
@@ -3,10 +3,10 @@ name: Move new issues into Issue triage board
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
automate-project-columns-next:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Move labelled issues to correct projects
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
call-triage-labelled:
|
||||
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
|
||||
|
||||
+102
@@ -1,3 +1,105 @@
|
||||
Changes in [34.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.13.0) (2024-12-03)
|
||||
====================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate `MatrixClient.isEventSenderVerified` ([#4527](https://github.com/matrix-org/matrix-js-sdk/pull/4527)). Contributed by @florianduros.
|
||||
* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Ensure we disambiguate display names which look like MXIDs ([#4540](https://github.com/matrix-org/matrix-js-sdk/pull/4540)). Contributed by @t3chguy.
|
||||
* Add `CryptoApi.getBackupInfo` ([#4512](https://github.com/matrix-org/matrix-js-sdk/pull/4512)). Contributed by @florianduros.
|
||||
* Fix local echo in embedded mode ([#4498](https://github.com/matrix-org/matrix-js-sdk/pull/4498)). Contributed by @toger5.
|
||||
* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix `RustBackupManager` remaining values after current backup removal ([#4537](https://github.com/matrix-org/matrix-js-sdk/pull/4537)). Contributed by @florianduros.
|
||||
|
||||
|
||||
Changes in [34.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.12.0) (2024-11-19)
|
||||
====================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate `MatrixClient.getKeyBackupVersion` ([#4505](https://github.com/matrix-org/matrix-js-sdk/pull/4505)). Contributed by @florianduros.
|
||||
* Deprecate unused callbacks in `CryptoCallbacks` ([#4501](https://github.com/matrix-org/matrix-js-sdk/pull/4501)). Contributed by @florianduros.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Handle M\_MAX\_DELAY\_EXCEEDED errors ([#4511](https://github.com/matrix-org/matrix-js-sdk/pull/4511)). Contributed by @AndrewFerr.
|
||||
* Allow configuration of MatrixRTC timers when calling joinRoomSession() ([#4510](https://github.com/matrix-org/matrix-js-sdk/pull/4510)). Contributed by @hughns.
|
||||
* When state says you've left ongoing call, rejoin ([#4342](https://github.com/matrix-org/matrix-js-sdk/pull/4342)). Contributed by @AndrewFerr.
|
||||
* Remove redundant type arguments in function call ([#4507](https://github.com/matrix-org/matrix-js-sdk/pull/4507)). Contributed by @AndrewFerr.
|
||||
* MatrixRTCSession: handle rate limit errors ([#4494](https://github.com/matrix-org/matrix-js-sdk/pull/4494)). Contributed by @AndrewFerr.
|
||||
* Send/receive error details with widgets ([#4492](https://github.com/matrix-org/matrix-js-sdk/pull/4492)). Contributed by @AndrewFerr.
|
||||
* Capture HTTP error response headers \& handle Retry-After header (MSC4041) ([#4471](https://github.com/matrix-org/matrix-js-sdk/pull/4471)). Contributed by @AndrewFerr.
|
||||
* Add RoomWidgetClient.sendToDeviceViaWidgetApi() ([#4475](https://github.com/matrix-org/matrix-js-sdk/pull/4475)). Contributed by @hughns.
|
||||
|
||||
|
||||
Changes in [34.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.11.1) (2024-11-12)
|
||||
====================================================================================================
|
||||
# Security
|
||||
- Fixes for [CVE-2024-50336](https://nvd.nist.gov/vuln/detail/CVE-2024-50336) / [GHSA-xvg8-m4x3-w6xr](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-xvg8-m4x3-w6xr).
|
||||
|
||||
Changes in [34.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.11.0) (2024-11-12)
|
||||
====================================================================================================
|
||||
# Security
|
||||
- Fixes for [CVE-2024-50336](https://nvd.nist.gov/vuln/detail/CVE-2024-50336) / [GHSA-xvg8-m4x3-w6xr](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-xvg8-m4x3-w6xr).
|
||||
|
||||
Changes in [34.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.10.0) (2024-11-05)
|
||||
====================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate `CreateSecretStorageOpts.keyBackupInfo` used in `CryptoApi.bootstrapSecretStorage.` ([#4474](https://github.com/matrix-org/matrix-js-sdk/pull/4474)). Contributed by @florianduros.
|
||||
* Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() ([#4380](https://github.com/matrix-org/matrix-js-sdk/pull/4380)). Contributed by @hughns.
|
||||
* Remove abandoned MSC3886, MSC3903, MSC3906 experimental implementations ([#4469](https://github.com/matrix-org/matrix-js-sdk/pull/4469)). Contributed by @t3chguy.
|
||||
* Deprecate `MatrixClient.getDehydratedDevice` ([#4467](https://github.com/matrix-org/matrix-js-sdk/pull/4467)). Contributed by @florianduros.
|
||||
* Deprecate top level crypto events re-export ([#4444](https://github.com/matrix-org/matrix-js-sdk/pull/4444)). Contributed by @florianduros.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() ([#4380](https://github.com/matrix-org/matrix-js-sdk/pull/4380)). Contributed by @hughns.
|
||||
* Do not rotate MatrixRTC media encryption key when a new member joins a session ([#4472](https://github.com/matrix-org/matrix-js-sdk/pull/4472)). Contributed by @hughns.
|
||||
* Avoid `<sender>|<session>` notation in log messages ([#4473](https://github.com/matrix-org/matrix-js-sdk/pull/4473)). Contributed by @richvdh.
|
||||
* Refactor/simplify Promises in MatrixRTCSession ([#4466](https://github.com/matrix-org/matrix-js-sdk/pull/4466)). Contributed by @AndrewFerr.
|
||||
* Prepare delayed call leave events more reliably ([#4447](https://github.com/matrix-org/matrix-js-sdk/pull/4447)). Contributed by @AndrewFerr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix DelayedEventInfo type ([#4446](https://github.com/matrix-org/matrix-js-sdk/pull/4446)). Contributed by @AndrewFerr.
|
||||
|
||||
|
||||
Changes in [34.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.9.0) (2024-10-22)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate the crypto events which are not used by the rust-crypto ([#4442](https://github.com/matrix-org/matrix-js-sdk/pull/4442)). Contributed by @florianduros.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix the rust crypto import in esm environments. ([#4445](https://github.com/matrix-org/matrix-js-sdk/pull/4445)). Contributed by @saul-jb.
|
||||
* Fix MatrixRTC sender key wrapping ([#4441](https://github.com/matrix-org/matrix-js-sdk/pull/4441)). Contributed by @hughns.
|
||||
|
||||
|
||||
Changes in [34.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.8.0) (2024-10-15)
|
||||
==================================================================================================
|
||||
This release removes insecure functionality, resolving CVE-2024-47080 / GHSA-4jf8-g8wp-cx7c.
|
||||
|
||||
Changes in [34.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.7.0) (2024-10-08)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys() ([#4427](https://github.com/matrix-org/matrix-js-sdk/pull/4427)). Contributed by @hughns.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Bump matrix-rust-sdk to 9.1.0 ([#4435](https://github.com/matrix-org/matrix-js-sdk/pull/4435)). Contributed by @richvdh.
|
||||
* Rotate Matrix RTC media encryption key when a new member joins a call for Post Compromise Security ([#4422](https://github.com/matrix-org/matrix-js-sdk/pull/4422)). Contributed by @hughns.
|
||||
* Update media event content types to include captions ([#4403](https://github.com/matrix-org/matrix-js-sdk/pull/4403)). Contributed by @tulir.
|
||||
* Update OIDC registration types to match latest MSC2966 state ([#4432](https://github.com/matrix-org/matrix-js-sdk/pull/4432)). Contributed by @t3chguy.
|
||||
* Add `CryptoApi.pinCurrentUserIdentity` and `UserIdentity.needsUserApproval` ([#4415](https://github.com/matrix-org/matrix-js-sdk/pull/4415)). Contributed by @richvdh.
|
||||
|
||||
|
||||
Changes in [34.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.6.0) (2024-09-24)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
@@ -38,8 +38,8 @@ client.publicRooms(function (err, data) {
|
||||
});
|
||||
```
|
||||
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
[the Node.js terminal app](examples/node/README.md) for a more complex example.
|
||||
See [below](#end-to-end-encryption-support) for how to enable end-to-end-encryption, or check
|
||||
[the Node.js terminal app](https://github.com/matrix-org/matrix-js-sdk/tree/develop/examples/node) for a more complex example.
|
||||
|
||||
To start the client:
|
||||
|
||||
@@ -303,44 +303,129 @@ Then visit `http://localhost:8005` to see the API docs.
|
||||
|
||||
# End-to-end encryption support
|
||||
|
||||
**This section is outdated.** Use of `libolm` is deprecated and we are replacing it with support
|
||||
from the matrix-rust-sdk (https://github.com/element-hq/element-web/issues/21972).
|
||||
`matrix-js-sdk`'s end-to-end encryption support is based on the [WebAssembly bindings](https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm) of the Rust [matrix-sdk-crypto](https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-crypto) library.
|
||||
|
||||
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the `Olm` global.
|
||||
## Initialization
|
||||
|
||||
It is also necessary to call `await matrixClient.initCrypto()` after creating a new
|
||||
`MatrixClient` (but **before** calling `matrixClient.startClient()`) to
|
||||
initialise the crypto layer.
|
||||
**Do not use `matrixClient.initCrypto()`. This method is deprecated and no longer maintained.**
|
||||
|
||||
If the `Olm` global is not available, the SDK will show a warning, as shown
|
||||
below; `initCrypto()` will also fail.
|
||||
To initialize the end-to-end encryption support in the matrix client:
|
||||
|
||||
```
|
||||
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
|
||||
```javascript
|
||||
// Create a new matrix client
|
||||
const matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
});
|
||||
|
||||
// Initialize to enable end-to-end encryption support.
|
||||
await matrixClient.initRustCrypto();
|
||||
```
|
||||
|
||||
If the crypto layer is not (successfully) initialised, the SDK will continue to
|
||||
work for unencrypted rooms, but it will not support the E2E parts of the Matrix
|
||||
specification.
|
||||
After calling `initRustCrypto`, you can obtain a reference to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html) interface, which is the main entry point for end-to-end encryption, by calling [`MatrixClient.getCrypto`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#getCrypto).
|
||||
|
||||
To provide the Olm library in a browser application:
|
||||
## Secret storage
|
||||
|
||||
- download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
|
||||
- load `olm.js` as a `<script>` _before_ `browser-matrix.js`.
|
||||
You should normally set up [secret storage](https://spec.matrix.org/v1.12/client-server-api/#secret-storage) before using the end-to-end encryption. To do this, call [`CryptoApi.bootstrapSecretStorage`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapSecretStorage).
|
||||
`bootstrapSecretStorage` can be called unconditionally: it will only set up the secret storage if it is not already set up (unless you use the `setupNewSecretStorage` parameter).
|
||||
|
||||
To provide the Olm library in a node.js application:
|
||||
```javascript
|
||||
const matrixClient = sdk.createClient({
|
||||
...,
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: async (keys) => {
|
||||
// This function should prompt the user to enter their secret storage key.
|
||||
return mySecretStorageKeys;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
- `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`
|
||||
(replace the URL with the latest version you want to use from
|
||||
https://packages.matrix.org/npm/olm/)
|
||||
- `global.Olm = require('olm');` _before_ loading `matrix-js-sdk`.
|
||||
matrixClient.getCrypto().bootstrapSecretStorage({
|
||||
// This function will be called if a new secret storage key (aka recovery key) is needed.
|
||||
// You should prompt the user to save the key somewhere, because they will need it to unlock secret storage in future.
|
||||
createSecretStorageKey: async () => {
|
||||
return mySecretStorageKey;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you want to package Olm as dependency for your node.js application, you can
|
||||
use `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`. If your
|
||||
application also works without e2e crypto enabled, add `--optional` to mark it
|
||||
as an optional dependency.
|
||||
The example above will create a new secret storage key if secret storage was not previously set up.
|
||||
The secret storage data will be encrypted using the secret storage key returned in [`createSecretStorageKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CreateSecretStorageOpts.html#createSecretStorageKey).
|
||||
|
||||
We recommend that you prompt the user to re-enter this key when [`CryptoCallbacks.getSecretStorageKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoCallbacks.html#getSecretStorageKey) is called (when the secret storage access is needed).
|
||||
|
||||
## Set up cross-signing
|
||||
|
||||
To set up cross-signing to verify devices and other users, call
|
||||
[`CryptoApi.bootstrapCrossSigning`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapCrossSigning):
|
||||
|
||||
```javascript
|
||||
matrixClient.getCrypto().bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
return makeRequest(authDict);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The [`authUploadDeviceSigningKeys`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.BootstrapCrossSigningOpts.html#authUploadDeviceSigningKeys)
|
||||
callback is required in order to upload newly-generated public cross-signing keys to the server.
|
||||
|
||||
## Key backup
|
||||
|
||||
If the user doesn't already have a [key backup](https://spec.matrix.org/v1.12/client-server-api/#server-side-key-backups) you should create one:
|
||||
|
||||
```javascript
|
||||
// Check if we have a key backup.
|
||||
// If checkKeyBackupAndEnable returns null, there is no key backup.
|
||||
const hasKeyBackup = (await matrixClient.getCrypto().checkKeyBackupAndEnable()) !== null;
|
||||
|
||||
// Create the key backup
|
||||
await matrixClient.getCrypto().resetKeyBackup();
|
||||
```
|
||||
|
||||
## Verify a new device
|
||||
|
||||
Once the cross-signing is set up on one of your devices, you can verify another device with two methods:
|
||||
|
||||
1. Use `CryptoApi.bootstrapCrossSigning`.
|
||||
|
||||
`bootstrapCrossSigning` will call the [CryptoCallbacks.getSecretStorageKey](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoCallbacks.html#getSecretStorageKey) callback. The device is verified with the private cross-signing keys fetched from the secret storage.
|
||||
|
||||
2. Request an interactive verification against existing devices, by calling [CryptoApi.requestOwnUserVerification](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#requestOwnUserVerification).
|
||||
|
||||
## Migrating from the legacy crypto stack to Rust crypto
|
||||
|
||||
If your application previously used the legacy crypto stack, (i.e, it called `MatrixClient.initCrypto()`), you will
|
||||
need to migrate existing devices to the Rust crypto stack.
|
||||
|
||||
This migration happens automatically when you call `initRustCrypto()` instead of `initCrypto()`,
|
||||
but you need to provide the legacy [`cryptoStore`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#cryptoStore) and [`pickleKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#pickleKey) to [`createClient`](https://matrix-org.github.io/matrix-js-sdk/functions/matrix.createClient.html):
|
||||
|
||||
```javascript
|
||||
// You should provide the legacy crypto store and the pickle key to the matrix client in order to migrate the data.
|
||||
const matrixClient = sdk.createClient({
|
||||
cryptoStore: myCryptoStore,
|
||||
pickleKey: myPickleKey,
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
});
|
||||
|
||||
// The migration will be done automatically when you call `initRustCrypto`.
|
||||
await matrixClient.initRustCrypto();
|
||||
```
|
||||
|
||||
To follow the migration progress, you can listen to the [`CryptoEvent.LegacyCryptoStoreMigrationProgress`](https://matrix-org.github.io/matrix-js-sdk/enums/crypto_api.CryptoEvent.html#LegacyCryptoStoreMigrationProgress) event:
|
||||
|
||||
```javascript
|
||||
// When progress === total === -1, the migration is finished.
|
||||
matrixClient.on(CryptoEvent.LegacyCryptoStoreMigrationProgress, (progress, total) => {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
The Rust crypto stack is not supported in a lot of deprecated methods of [`MatrixClient`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html). If you use them, you should migrate to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html). Also, the legacy `MatrixClient.crypto` object is not available any more: you should use `MatrixClient.getCrypto()` instead.
|
||||
|
||||
# Contributing
|
||||
|
||||
|
||||
@@ -31,5 +31,22 @@ module.exports = {
|
||||
"@babel/plugin-transform-object-rest-spread",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime",
|
||||
[
|
||||
"search-and-replace",
|
||||
{
|
||||
// Since rewriteImportExtensions doesn't work on dynamic imports (yet), we need to manually replace
|
||||
// the dynamic rust-crypto import.
|
||||
// (see https://github.com/babel/babel/issues/16750)
|
||||
rules:
|
||||
process.env.NODE_ENV !== "test"
|
||||
? [
|
||||
{
|
||||
search: "./rust-crypto/index.ts",
|
||||
replace: "./rust-crypto/index.js",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
+12
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "34.7.0-rc.0",
|
||||
"version": "34.13.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -58,12 +58,12 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.8.2",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "10"
|
||||
"uuid": "11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.6.0",
|
||||
@@ -81,6 +81,7 @@
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@peculiar/webcrypto": "^1.4.5",
|
||||
"@stylistic/eslint-plugin": "^2.9.0",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
@@ -88,23 +89,24 @@
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babel-plugin-search-and-replace": "^1.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"eslint": "8.57.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
"eslint-plugin-jsdoc": "^50.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-matrix-org": "^2.0.1",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.3.0",
|
||||
"eslint-plugin-unicorn": "^55.0.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "11.1.3",
|
||||
"fetch-mock": "11.1.5",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^29.0.0",
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
|
||||
for i in main typings browser
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field earlier at publish time, so we should revert it now.
|
||||
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
||||
# If there's a `src` prefixed value, use that, otherwise delete.
|
||||
# This is used to delete the `typings` field and reset `main` back to the TypeScript source.
|
||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||
if [ "$src_value" != "null" ]; then
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
else
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$(git ls-files --modified package.json)" ]; then
|
||||
echo "Committing develop package.json"
|
||||
git commit package.json -m "Resetting package fields for development"
|
||||
fi
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# For the published and dist versions of the package,
|
||||
# we copy the `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if they exist).
|
||||
# This small bit of gymnastics allows us to use the TypeScript source directly for development without
|
||||
# needing to build before linting or testing.
|
||||
|
||||
for i in main typings browser
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
fi
|
||||
done
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fsProm = require("fs/promises");
|
||||
|
||||
const PKGJSON = "package.json";
|
||||
|
||||
async function main() {
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
|
||||
for (const field of ["main", "typings"]) {
|
||||
if (pkgJson["matrix_lib_" + field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_" + field];
|
||||
}
|
||||
}
|
||||
|
||||
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1 +0,0 @@
|
||||
switch_package_to_release.cjs
|
||||
+1
-1
@@ -66,7 +66,7 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: this.httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
...options,
|
||||
};
|
||||
if (!fullOptions.cryptoStore) {
|
||||
|
||||
@@ -91,7 +91,6 @@ import {
|
||||
OnlySignedDevicesIsolationMode,
|
||||
} from "../../../src/crypto-api";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
import {
|
||||
createOlmAccount,
|
||||
createOlmSession,
|
||||
@@ -106,6 +105,7 @@ import { ToDevicePayload } from "../../../src/models/ToDeviceMessage";
|
||||
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
|
||||
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
import { KeyBackup } from "../../../src/rust-crypto/backup.ts";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -136,7 +136,7 @@ async function expectSendRoomKey(
|
||||
recipientOlmAccount: Olm.Account,
|
||||
recipientOlmSession: Olm.Session | null = null,
|
||||
): Promise<Olm.InboundGroupSession> {
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
|
||||
|
||||
function onSendRoomKey(content: any): Olm.InboundGroupSession {
|
||||
@@ -219,7 +219,7 @@ async function expectSendMegolmMessage(
|
||||
}
|
||||
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
// currently we use libolm to implement the crypto in the tests, so need it to be present.
|
||||
logger.warn("not running megolm tests: Olm not present");
|
||||
return;
|
||||
@@ -230,7 +230,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
|
||||
const newBackendOnly = backend !== "rust-sdk" ? test.skip : test;
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
let testOlmAccount = {} as unknown as Olm.Account;
|
||||
let testSenderKey = "";
|
||||
@@ -1897,13 +1897,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const aliceOtks = await keyReceiver.awaitOneTimeKeyUpload();
|
||||
const aliceOtkId = Object.keys(aliceOtks)[0];
|
||||
const aliceOtk = aliceOtks[aliceOtkId];
|
||||
const p2pSession = new global.Olm.Session();
|
||||
const p2pSession = new globalThis.Olm.Session();
|
||||
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
|
||||
"readonly",
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
|
||||
const account = new global.Olm.Account();
|
||||
const account = new globalThis.Olm.Account();
|
||||
try {
|
||||
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
|
||||
p2pSession.create_outbound(account, keyReceiver.getDeviceKey(), aliceOtk.key);
|
||||
@@ -2045,13 +2045,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const aliceOtks = await keyReceiver.awaitOneTimeKeyUpload();
|
||||
const aliceOtkId = Object.keys(aliceOtks)[0];
|
||||
const aliceOtk = aliceOtks[aliceOtkId];
|
||||
const p2pSession = new global.Olm.Session();
|
||||
const p2pSession = new globalThis.Olm.Session();
|
||||
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
|
||||
"readonly",
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
|
||||
const account = new global.Olm.Account();
|
||||
const account = new globalThis.Olm.Account();
|
||||
try {
|
||||
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
|
||||
p2pSession.create_outbound(account, keyReceiver.getDeviceKey(), aliceOtk.key);
|
||||
@@ -3138,11 +3138,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// Import a new key that should be uploaded
|
||||
const newKey = testData.MEGOLM_SESSION_DATA;
|
||||
|
||||
const awaitKeyUploaded = new Promise<IKeyBackup>((resolve) => {
|
||||
const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
(url, request) => {
|
||||
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
resolve(uploadPayload);
|
||||
return {
|
||||
status: 200,
|
||||
@@ -3499,7 +3499,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
return client;
|
||||
}
|
||||
|
||||
function mkEncryptionEvent(content: Object) {
|
||||
function mkEncryptionEvent(content: object) {
|
||||
return mkEventCustom({
|
||||
sender: persistentStoreClient.getSafeUserId(),
|
||||
type: "m.room.encryption",
|
||||
@@ -3512,7 +3512,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
*
|
||||
* @param stateEvents - Additional state events for the test room
|
||||
*/
|
||||
function getSyncResponseWithState(stateEvents: Array<Object>) {
|
||||
function getSyncResponseWithState(stateEvents: Array<object>) {
|
||||
const roomResponse = {
|
||||
state: {
|
||||
events: [
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Mocked } from "jest-mock";
|
||||
import {
|
||||
createClient,
|
||||
Crypto,
|
||||
CryptoEvent,
|
||||
encodeBase64,
|
||||
ICreateClientOpts,
|
||||
IEvent,
|
||||
IMegolmSessionData,
|
||||
@@ -42,10 +42,10 @@ import {
|
||||
} from "../../test-utils/test-utils";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { defer, IDeferred } from "../../../src/utils";
|
||||
import { DecryptionFailureCode } from "../../../src/crypto-api";
|
||||
import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent } from "../../../src/crypto-api";
|
||||
import { KeyBackup } from "../../../src/rust-crypto/backup.ts";
|
||||
|
||||
const ROOM_ID = testData.TEST_ROOM_ID;
|
||||
|
||||
@@ -91,7 +91,7 @@ function mockUploadEmitter(
|
||||
},
|
||||
};
|
||||
}
|
||||
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
let count = 0;
|
||||
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
|
||||
for (const sessionId of Object.keys(value.sessions)) {
|
||||
@@ -117,8 +117,10 @@ function mockUploadEmitter(
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
|
||||
// Rust backend. Once we have full support in the rust sdk, it will go away.
|
||||
// const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
|
||||
// const newBackendOnly = backend === "libolm" ? test.skip : test;
|
||||
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
|
||||
const newBackendOnly = backend === "libolm" ? test.skip : test;
|
||||
|
||||
const isNewBackend = backend === "rust-sdk";
|
||||
|
||||
let aliceClient: MatrixClient;
|
||||
/** an object which intercepts `/sync` requests on the test homeserver */
|
||||
@@ -247,9 +249,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
// On the first decryption attempt, decryption fails.
|
||||
await awaitDecryption(event);
|
||||
expect(event.decryptionFailureReason).toEqual(
|
||||
backend === "libolm"
|
||||
? DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID
|
||||
: DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
|
||||
isNewBackend
|
||||
? DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP
|
||||
: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
);
|
||||
|
||||
// Eventually, decryption succeeds.
|
||||
@@ -314,6 +316,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.get(
|
||||
`path:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`,
|
||||
testData.SIGNED_BACKUP_DATA,
|
||||
);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
aliceCrypto = aliceClient.getCrypto()!;
|
||||
@@ -344,20 +350,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
onKeyCached = resolve;
|
||||
});
|
||||
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const result = await advanceTimersUntil(
|
||||
aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
cacheCompleteCallback: () => onKeyCached(),
|
||||
},
|
||||
),
|
||||
isNewBackend
|
||||
? aliceCrypto.restoreKeyBackup()
|
||||
: aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
cacheCompleteCallback: () => onKeyCached(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
|
||||
if (isNewBackend) return;
|
||||
|
||||
await awaitKeyCached;
|
||||
|
||||
// The key should be now cached
|
||||
@@ -398,8 +413,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
it("Should import full backup in chunks", async function () {
|
||||
const importMockImpl = jest.fn();
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.importBackedUpRoomKeys = importMockImpl;
|
||||
if (isNewBackend) {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
} else {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
jest.spyOn(aliceCrypto, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
}
|
||||
|
||||
// We need several rooms with several sessions to test chunking
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
|
||||
@@ -408,17 +428,26 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
progressCallback,
|
||||
},
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const result = await (isNewBackend
|
||||
? aliceCrypto.restoreKeyBackup({
|
||||
progressCallback,
|
||||
})
|
||||
: aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
progressCallback,
|
||||
},
|
||||
));
|
||||
|
||||
expect(result.imported).toStrictEqual(expectedTotal);
|
||||
// Should be called 5 times: 200*4 plus one chunk with the remaining 32
|
||||
expect(importMockImpl).toHaveBeenCalledTimes(5);
|
||||
@@ -451,8 +480,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
|
||||
it("Should continue to process backup if a chunk import fails and report failures", async function () {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.importBackedUpRoomKeys = jest
|
||||
const importMockImpl = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// Fail to import first chunk
|
||||
@@ -461,22 +489,36 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
// Ok for other chunks
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
if (isNewBackend) {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
} else {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
jest.spyOn(aliceCrypto, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
}
|
||||
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
progressCallback,
|
||||
},
|
||||
);
|
||||
const result = await (isNewBackend
|
||||
? aliceCrypto.restoreKeyBackup({ progressCallback })
|
||||
: aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
progressCallback,
|
||||
},
|
||||
));
|
||||
|
||||
expect(result.total).toStrictEqual(expectedTotal);
|
||||
// A chunk failed to import
|
||||
@@ -527,20 +569,26 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const result = await (isNewBackend
|
||||
? aliceCrypto.restoreKeyBackup()
|
||||
: aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
));
|
||||
|
||||
expect(result.total).toStrictEqual(expectedTotal);
|
||||
// A chunk failed to import
|
||||
expect(result.imported).toStrictEqual(expectedTotal - decryptionFailureCount);
|
||||
});
|
||||
|
||||
it("recover specific session from backup", async function () {
|
||||
oldBackendOnly("recover specific session from backup", async function () {
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
@@ -560,7 +608,33 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it("Fails on bad recovery key", async function () {
|
||||
newBackendOnly(
|
||||
"Should get the decryption key from the secret storage and restore the key backup",
|
||||
async function () {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
jest.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
|
||||
|
||||
const fullBackup = {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
||||
const decryptionKey = await aliceCrypto.getSessionBackupPrivateKey();
|
||||
expect(encodeBase64(decryptionKey!)).toStrictEqual(testData.BACKUP_DECRYPTION_KEY_BASE64);
|
||||
|
||||
const result = await aliceCrypto.restoreKeyBackup();
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
},
|
||||
);
|
||||
|
||||
oldBackendOnly("Fails on bad recovery key", async function () {
|
||||
const fullBackup = {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
@@ -584,6 +658,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
newBackendOnly("Should throw an error if the decryption key is not found in cache", async () => {
|
||||
await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in crypto store");
|
||||
});
|
||||
});
|
||||
|
||||
describe("backupLoop", () => {
|
||||
@@ -890,6 +968,40 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
expect(backupStatus).toStrictEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
});
|
||||
|
||||
newBackendOnly("getKeyBackupInfo() should not return a backup if the active backup has been deleted", async () => {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
fetchMock.delete(`express:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`, {});
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
// At this point there is no backup
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
|
||||
|
||||
// Return now the backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toStrictEqual(testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// Delete the backup and we are expecting the key backup to be disabled
|
||||
const keyBackupStatus = defer<boolean>();
|
||||
aliceClient.once(CryptoEvent.KeyBackupStatus, (enabled) => keyBackupStatus.resolve(enabled));
|
||||
await aliceCrypto.deleteKeyBackupVersion(testData.SIGNED_BACKUP_DATA.version!);
|
||||
expect(await keyBackupStatus.promise).toBe(false);
|
||||
|
||||
// The backup info should not be available anymore
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
|
||||
});
|
||||
|
||||
describe("isKeyBackupTrusted", () => {
|
||||
it("does not trust a backup signed by an untrusted device", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
|
||||
@@ -85,15 +85,15 @@ export function bootstrapCrossSigningTestOlmAccount(
|
||||
deviceId: string,
|
||||
keyBackupInfo: KeyBackupInfo[] = [],
|
||||
): Partial<IDownloadKeyResult> {
|
||||
const olmAliceMSK = new global.Olm.PkSigning();
|
||||
const olmAliceMSK = new globalThis.Olm.PkSigning();
|
||||
const masterPrivkey = olmAliceMSK.generate_seed();
|
||||
const masterPubkey = olmAliceMSK.init_with_seed(masterPrivkey);
|
||||
|
||||
const olmAliceUSK = new global.Olm.PkSigning();
|
||||
const olmAliceUSK = new globalThis.Olm.PkSigning();
|
||||
const userPrivkey = olmAliceUSK.generate_seed();
|
||||
const userPubkey = olmAliceUSK.init_with_seed(userPrivkey);
|
||||
|
||||
const olmAliceSSK = new global.Olm.PkSigning();
|
||||
const olmAliceSSK = new globalThis.Olm.PkSigning();
|
||||
const sskPrivkey = olmAliceSSK.generate_seed();
|
||||
const sskPubkey = olmAliceSSK.init_with_seed(sskPrivkey);
|
||||
|
||||
@@ -181,7 +181,7 @@ export async function createOlmSession(
|
||||
const otkId = Object.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new global.Olm.Session();
|
||||
const session = new globalThis.Olm.Session();
|
||||
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import { createClient, MatrixClient } from "../../../src";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
/**
|
||||
* Integration tests for to-device messages functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
beforeEach(
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: testData.TEST_USER_ID,
|
||||
accessToken: "akjgkrgjsalice",
|
||||
deviceId: testData.TEST_DEVICE_ID,
|
||||
});
|
||||
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
new E2EKeyReceiver(homeserverUrl);
|
||||
const syncResponder = new SyncResponder(homeserverUrl);
|
||||
|
||||
// add bob as known user
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
|
||||
|
||||
// Silence warnings from the backup manager
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND" },
|
||||
});
|
||||
|
||||
fetchMock.get(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.get(new URL("/_matrix/client/versions/", homeserverUrl).toString(), {});
|
||||
fetchMock.post(
|
||||
new URL(
|
||||
`/_matrix/client/v3/user/${encodeURIComponent(testData.TEST_USER_ID)}/filter`,
|
||||
homeserverUrl,
|
||||
).toString(),
|
||||
{ filter_id: "fid" },
|
||||
);
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
},
|
||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||
10000,
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe("encryptToDeviceMessages", () => {
|
||||
it("returns empty batch for device that is not known", async () => {
|
||||
await aliceClient.startClient();
|
||||
|
||||
const toDeviceBatch = await aliceClient
|
||||
.getCrypto()
|
||||
?.encryptToDeviceMessages(
|
||||
"m.test.event",
|
||||
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
|
||||
{
|
||||
some: "content",
|
||||
},
|
||||
);
|
||||
|
||||
expect(toDeviceBatch).toBeDefined();
|
||||
const { batch, eventType } = toDeviceBatch!;
|
||||
expect(eventType).toBe("m.room.encrypted");
|
||||
expect(batch.length).toBe(0);
|
||||
});
|
||||
|
||||
it("returns encrypted batch for known device", async () => {
|
||||
await aliceClient.startClient();
|
||||
e2eKeyResponder.addDeviceKeys(testData.BOB_SIGNED_TEST_DEVICE_DATA);
|
||||
fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({
|
||||
one_time_keys: testData.BOB_ONE_TIME_KEYS,
|
||||
}));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const toDeviceBatch = await aliceClient
|
||||
.getCrypto()
|
||||
?.encryptToDeviceMessages(
|
||||
"m.test.event",
|
||||
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
|
||||
{
|
||||
some: "content",
|
||||
},
|
||||
);
|
||||
|
||||
expect(toDeviceBatch?.batch.length).toBe(1);
|
||||
expect(toDeviceBatch?.eventType).toBe("m.room.encrypted");
|
||||
const { deviceId, payload, userId } = toDeviceBatch!.batch[0];
|
||||
expect(deviceId).toBe(testData.BOB_TEST_DEVICE_ID);
|
||||
expect(userId).toBe(testData.BOB_TEST_USER_ID);
|
||||
expect(payload.algorithm).toBe("m.olm.v1.curve25519-aes-sha2");
|
||||
expect(payload.sender_key).toEqual(expect.any(String));
|
||||
expect(payload.ciphertext).toEqual(
|
||||
expect.objectContaining({
|
||||
[testData.BOB_SIGNED_TEST_DEVICE_DATA.keys[`curve25519:${testData.BOB_TEST_DEVICE_ID}`]]: {
|
||||
body: expect.any(String),
|
||||
type: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// for future: check that bob's device can decrypt the ciphertext?
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -90,11 +90,12 @@ jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
beforeAll(async () => {
|
||||
// we use the libolm primitives in the test, so init the Olm library
|
||||
await global.Olm.init();
|
||||
await globalThis.Olm.init();
|
||||
});
|
||||
|
||||
// load the rust library. This can take a few seconds on a slow GH worker.
|
||||
beforeAll(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const RustSdkCryptoJs = await require("@matrix-org/matrix-sdk-crypto-wasm");
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
}, 10000);
|
||||
@@ -263,7 +264,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
|
||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const olmSAS = new globalThis.Olm.SAS();
|
||||
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
|
||||
|
||||
// alice responds with a 'key' ...
|
||||
@@ -357,7 +358,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
// The dummy device makes up a curve25519 keypair and uses the hash in an 'm.key.verification.accept'
|
||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const olmSAS = new globalThis.Olm.SAS();
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage);
|
||||
|
||||
sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key");
|
||||
|
||||
@@ -67,7 +67,7 @@ function getSyncResponse(roomMembers: string[]) {
|
||||
}
|
||||
|
||||
describe("DeviceList management:", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("not running deviceList tests: Olm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ describe("MatrixClient opts", function () {
|
||||
let client: MatrixClient;
|
||||
beforeEach(function () {
|
||||
client = new MatrixClient({
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
store: undefined,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
@@ -135,7 +135,7 @@ describe("MatrixClient opts", function () {
|
||||
let client: MatrixClient;
|
||||
beforeEach(function () {
|
||||
client = new MatrixClient({
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
store: new MemoryStore() as IStore,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
|
||||
@@ -577,7 +577,7 @@ describe("MatrixClient syncing", () => {
|
||||
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]).then(() => {
|
||||
const member = client!.getRoom(roomOne)!.getMember(userC)!;
|
||||
expect(member.name).toEqual("The Boss");
|
||||
expect(member.getAvatarUrl("home.server.url", 1, 1, "", false, false)).toBeTruthy();
|
||||
expect(member.getAvatarUrl("https://home.server.url", 1, 1, "", false, false)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2406,7 +2406,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
|
||||
it("should emit ClientEvent.Room when invited while using indexeddb crypto store", async () => {
|
||||
const idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
cryptoStore: new IndexedDBCryptoStore(global.indexedDB, "tests"),
|
||||
cryptoStore: new IndexedDBCryptoStore(globalThis.indexedDB, "tests"),
|
||||
});
|
||||
const idbHttpBackend = idbTestClient.httpBackend;
|
||||
const idbClient = idbTestClient.client;
|
||||
@@ -2486,7 +2486,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
|
||||
let idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
store: new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
indexedDB: globalThis.indexedDB,
|
||||
dbName: "test",
|
||||
}),
|
||||
});
|
||||
@@ -2558,7 +2558,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
|
||||
idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
store: new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
indexedDB: globalThis.indexedDB,
|
||||
dbName: "test",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -266,8 +266,8 @@ describe("MSC4108SignInWithQR", () => {
|
||||
});
|
||||
|
||||
it("should abort if device doesn't come up by timeout", async () => {
|
||||
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
|
||||
(<Function>fn)();
|
||||
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
});
|
||||
@@ -319,8 +319,8 @@ describe("MSC4108SignInWithQR", () => {
|
||||
});
|
||||
|
||||
it("should not send secrets if user cancels", async () => {
|
||||
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
|
||||
(<Function>fn)();
|
||||
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
});
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import { logger } from "../src/logger";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
globalThis.Olm = require("@matrix-org/olm");
|
||||
logger.log("loaded libolm");
|
||||
} catch (e) {
|
||||
|
||||
@@ -27,7 +27,7 @@ export class AccountDataAccumulator {
|
||||
* Will be updated when fetchMock intercepts calls to PUT `/_matrix/client/v3/user/:userId/account_data/`.
|
||||
* Will be used by `sendSyncResponseWithUpdatedAccountData`
|
||||
*/
|
||||
public accountDataEvents: Map<String, any> = new Map();
|
||||
public accountDataEvents: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* Intercept requests to set a particular type of account data.
|
||||
@@ -101,7 +101,7 @@ export class AccountDataAccumulator {
|
||||
})),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// Might fail with "Cannot queue more than one /sync response" if called too often.
|
||||
// It's ok if it fails here, the sync response is cumulative and will contain
|
||||
// the latest account data.
|
||||
|
||||
@@ -127,7 +127,7 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = jest.fn();
|
||||
}
|
||||
} catch (ex) {
|
||||
} catch {
|
||||
// Direct access to some non-function fields of DOM prototypes may
|
||||
// cause exceptions.
|
||||
// Overwriting will not work either in that case.
|
||||
@@ -560,7 +560,7 @@ export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
|
||||
export type InitCrypto = (_: MatrixClient) => Promise<void>;
|
||||
|
||||
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
|
||||
if (global.Olm) {
|
||||
if (globalThis.Olm) {
|
||||
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
|
||||
}
|
||||
|
||||
|
||||
@@ -582,11 +582,11 @@ export class MockCallFeed {
|
||||
}
|
||||
|
||||
export function installWebRTCMocks() {
|
||||
global.navigator = {
|
||||
globalThis.navigator = {
|
||||
mediaDevices: new MockMediaDevices().typed(),
|
||||
} as unknown as Navigator;
|
||||
|
||||
global.window = {
|
||||
globalThis.window = {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
@@ -596,13 +596,13 @@ export function installWebRTCMocks() {
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
globalThis.document = {};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.AudioContext = MockAudioContext;
|
||||
globalThis.AudioContext = MockAudioContext;
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpReceiver = {
|
||||
globalThis.RTCRtpReceiver = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
@@ -610,7 +610,7 @@ export function installWebRTCMocks() {
|
||||
};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpSender = {
|
||||
globalThis.RTCRtpSender = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
|
||||
@@ -9,7 +9,7 @@ import { defer } from "../../src/utils";
|
||||
|
||||
describe("onResumedSync", () => {
|
||||
let batch: IndexedToDeviceBatch | null;
|
||||
let shouldFailSendToDevice: Boolean;
|
||||
let shouldFailSendToDevice: boolean;
|
||||
let onSendToDeviceFailure: () => void;
|
||||
let onSendToDeviceSuccess: () => void;
|
||||
let resumeSync: (newState: SyncState, oldState: SyncState) => void;
|
||||
|
||||
@@ -22,12 +22,12 @@ import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
// keep to reset the fetch function after using MockHttpBackend
|
||||
// @ts-ignore private property
|
||||
const realAutoDiscoveryFetch: typeof global.fetch = AutoDiscovery.fetchFn;
|
||||
const realAutoDiscoveryFetch: typeof globalThis.fetch = AutoDiscovery.fetchFn;
|
||||
|
||||
describe("AutoDiscovery", function () {
|
||||
const getHttpBackend = (): MockHttpBackend => {
|
||||
const httpBackend = new MockHttpBackend();
|
||||
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
|
||||
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof globalThis.fetch);
|
||||
return httpBackend;
|
||||
};
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
Buffer = undefined;
|
||||
|
||||
global.atob = NodeBuffer.atob;
|
||||
global.btoa = NodeBuffer.btoa;
|
||||
globalThis.atob = NodeBuffer.atob;
|
||||
globalThis.btoa = NodeBuffer.btoa;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,9 +38,9 @@ describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
Buffer = origBuffer;
|
||||
// @ts-ignore
|
||||
global.atob = undefined;
|
||||
globalThis.atob = undefined;
|
||||
// @ts-ignore
|
||||
global.btoa = undefined;
|
||||
globalThis.btoa = undefined;
|
||||
});
|
||||
|
||||
it("Should decode properly encoded data", () => {
|
||||
|
||||
@@ -29,10 +29,10 @@ describe("Beacon content helpers", () => {
|
||||
describe("makeBeaconInfoContent()", () => {
|
||||
const mockDateNow = 123456789;
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(mockDateNow);
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
jest.spyOn(globalThis.Date, "now").mockRestore();
|
||||
});
|
||||
it("create fully defined event content", () => {
|
||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
|
||||
|
||||
@@ -63,20 +63,6 @@ describe("ContentRepo", function () {
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs after any query parameters", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32#automade",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs at the end of the HTTP URI", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return an authenticated URL when requested", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, true, true)).toEqual(
|
||||
@@ -98,5 +84,30 @@ describe("ContentRepo", function () {
|
||||
"/_matrix/client/v1/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should drop mxc urls with invalid server_name", () => {
|
||||
const mxcUri = "mxc://server.name:test/foobar";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
|
||||
});
|
||||
|
||||
it("should drop mxc urls with invalid media_id", () => {
|
||||
const mxcUri = "mxc://server.name/foobar:test";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
|
||||
});
|
||||
|
||||
it("should drop mxc urls attempting a path traversal attack", () => {
|
||||
const mxcUri = "mxc://../../../../foo";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
|
||||
});
|
||||
|
||||
it("should drop mxc urls attempting to pass query parameters", () => {
|
||||
const mxcUri = "mxc://server.name/foobar?bar=baz";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
|
||||
});
|
||||
|
||||
it("should drop mxc urls with too many parts", () => {
|
||||
const mxcUri = "mxc://server.name/foo//bar";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+114
-2
@@ -26,8 +26,9 @@ import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend";
|
||||
import * as testData from "../test-utils/test-data";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
import type { DeviceInfoMap } from "../../src/crypto/DeviceList";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
function awaitEvent(emitter: EventEmitter, event: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
@@ -607,7 +608,7 @@ describe("Crypto", function () {
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
@@ -1245,6 +1246,117 @@ describe("Crypto", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptToDeviceMessages", () => {
|
||||
let client: TestClient;
|
||||
let ensureOlmSessionsForDevices: jest.SpiedFunction<typeof olmlib.ensureOlmSessionsForDevices>;
|
||||
let encryptMessageForDevice: jest.SpiedFunction<typeof olmlib.encryptMessageForDevice>;
|
||||
const payload = { hello: "world" };
|
||||
let encryptedPayload: object;
|
||||
let crypto: Crypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
|
||||
ensureOlmSessionsForDevices.mockResolvedValue(new Map());
|
||||
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
|
||||
encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => {
|
||||
result.plaintext = { type: 0, body: JSON.stringify(payload) };
|
||||
});
|
||||
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
|
||||
// running initCrypto should trigger a key upload
|
||||
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
|
||||
await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
|
||||
|
||||
encryptedPayload = {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } },
|
||||
};
|
||||
|
||||
crypto = client.client.getCrypto() as Crypto;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
ensureOlmSessionsForDevices.mockRestore();
|
||||
encryptMessageForDevice.mockRestore();
|
||||
await client.stop();
|
||||
});
|
||||
|
||||
it("returns encrypted batch where devices known", async () => {
|
||||
const deviceInfoMap: DeviceInfoMap = new Map([
|
||||
[
|
||||
"@bob:example.org",
|
||||
new Map([
|
||||
["bobweb", new DeviceInfo("bobweb")],
|
||||
["bobmobile", new DeviceInfo("bobmobile")],
|
||||
]),
|
||||
],
|
||||
["@carol:example.org", new Map([["caroldesktop", new DeviceInfo("caroldesktop")]])],
|
||||
]);
|
||||
jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(deviceInfoMap);
|
||||
// const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false);
|
||||
|
||||
const batch = await client.client.getCrypto()?.encryptToDeviceMessages(
|
||||
"m.test.type",
|
||||
[
|
||||
{ userId: "@bob:example.org", deviceId: "bobweb" },
|
||||
{ userId: "@bob:example.org", deviceId: "bobmobile" },
|
||||
{ userId: "@carol:example.org", deviceId: "caroldesktop" },
|
||||
{ userId: "@carol:example.org", deviceId: "carolmobile" }, // not known
|
||||
],
|
||||
payload,
|
||||
);
|
||||
expect(crypto.deviceList.downloadKeys).toHaveBeenCalledWith(
|
||||
["@bob:example.org", "@carol:example.org"],
|
||||
false,
|
||||
);
|
||||
expect(encryptMessageForDevice).toHaveBeenCalledTimes(3);
|
||||
const expectedPayload = expect.objectContaining({
|
||||
...encryptedPayload,
|
||||
"org.matrix.msgid": expect.any(String),
|
||||
"sender_key": expect.any(String),
|
||||
});
|
||||
expect(batch?.eventType).toEqual("m.room.encrypted");
|
||||
expect(batch?.batch.length).toEqual(3);
|
||||
expect(batch).toEqual({
|
||||
eventType: "m.room.encrypted",
|
||||
batch: expect.arrayContaining([
|
||||
{
|
||||
userId: "@bob:example.org",
|
||||
deviceId: "bobweb",
|
||||
payload: expectedPayload,
|
||||
},
|
||||
{
|
||||
userId: "@bob:example.org",
|
||||
deviceId: "bobmobile",
|
||||
payload: expectedPayload,
|
||||
},
|
||||
{
|
||||
userId: "@carol:example.org",
|
||||
deviceId: "caroldesktop",
|
||||
payload: expectedPayload,
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it("returns empty batch if no devices known", async () => {
|
||||
jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(new Map());
|
||||
const batch = await crypto.encryptToDeviceMessages(
|
||||
"m.test.type",
|
||||
[
|
||||
{ deviceId: "AAA", userId: "@user1:domain" },
|
||||
{ deviceId: "BBB", userId: "@user1:domain" },
|
||||
{ deviceId: "CCC", userId: "@user2:domain" },
|
||||
],
|
||||
payload,
|
||||
);
|
||||
expect(batch?.eventType).toEqual("m.room.encrypted");
|
||||
expect(batch?.batch).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkSecretStoragePrivateKey", () => {
|
||||
let client: TestClient;
|
||||
|
||||
|
||||
@@ -44,13 +44,13 @@ badKey[0] ^= 1;
|
||||
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
it("should throw if no callback is provided", async () => {
|
||||
@@ -80,7 +80,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
// check that the pkSigning object corresponds to the pubKey
|
||||
const signature = pkSigning.sign("message");
|
||||
const util = new global.Olm.Utility();
|
||||
const util = new globalThis.Olm.Utility();
|
||||
try {
|
||||
util.ed25519_verify(pubKey, "message", signature);
|
||||
} finally {
|
||||
@@ -199,7 +199,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
* it's not possible to get one in normal execution unless you hack as we do here.
|
||||
*/
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore", () => new IndexedDBCryptoStore(undefined!, "tests")],
|
||||
[
|
||||
"MemoryCryptoStore",
|
||||
|
||||
@@ -43,10 +43,10 @@ const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2
|
||||
|
||||
const ROOM_ID = "!ROOM:ID";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
describe("MegolmDecryption", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
@@ -101,7 +101,7 @@ describe("MegolmDecryption", function () {
|
||||
describe("receives some keys:", function () {
|
||||
let groupSession: OutboundGroupSession;
|
||||
beforeEach(async function () {
|
||||
groupSession = new global.Olm.OutboundGroupSession();
|
||||
groupSession = new globalThis.Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// construct a fake decrypted key event via the use of a mocked
|
||||
|
||||
@@ -47,13 +47,13 @@ function alwaysSucceed<T>(promise: Promise<T>): Promise<T | void> {
|
||||
}
|
||||
|
||||
describe("OlmDevice", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
let aliceOlmDevice: OlmDevice;
|
||||
|
||||
@@ -33,7 +33,7 @@ import { CryptoStore } from "../../../src/crypto/store/base";
|
||||
import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm";
|
||||
import { IKeyBackupInfo } from "../../../src/crypto/keybackup";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
|
||||
@@ -155,7 +155,7 @@ function makeTestClient(cryptoStore: CryptoStore) {
|
||||
}
|
||||
|
||||
describe("MegolmBackup", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
@@ -205,14 +205,14 @@ describe("MegolmBackup", function () {
|
||||
// clobber the setTimeout function to run 100x faster.
|
||||
// ideally we would use lolex, but we have no oportunity
|
||||
// to tick the clock between the first try and the retry.
|
||||
const realSetTimeout = global.setTimeout;
|
||||
jest.spyOn(global, "setTimeout").mockImplementation(function (f, n) {
|
||||
const realSetTimeout = globalThis.setTimeout;
|
||||
jest.spyOn(globalThis, "setTimeout").mockImplementation(function (f, n) {
|
||||
return realSetTimeout(f!, n! / 100);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
jest.spyOn(global, "setTimeout").mockRestore();
|
||||
jest.spyOn(globalThis, "setTimeout").mockRestore();
|
||||
});
|
||||
|
||||
test("fail if crypto not enabled", async () => {
|
||||
@@ -780,4 +780,12 @@ describe("MegolmBackup", function () {
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getKeyBackupInfo", () => {
|
||||
it("should return throw an `Not implemented`", async () => {
|
||||
const client = makeTestClient(cryptoStore);
|
||||
await client.initCrypto();
|
||||
await expect(client.getCrypto()?.getKeyBackupInfo()).rejects.toThrow("Not implemented");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,13 +84,13 @@ async function makeTestClient(
|
||||
}
|
||||
|
||||
describe("Cross Signing", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
it("should sign the master key with the device key", async function () {
|
||||
@@ -140,9 +140,7 @@ describe("Cross Signing", function () {
|
||||
});
|
||||
}
|
||||
|
||||
const error = new MatrixError(errorResponse);
|
||||
error.httpStatus == 401;
|
||||
throw error;
|
||||
throw new MatrixError(errorResponse, 401);
|
||||
};
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
@@ -371,10 +369,10 @@ describe("Cross Signing", function () {
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobSigning = new globalThis.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK: CrossSigningKeyInfo = {
|
||||
@@ -490,7 +488,7 @@ describe("Cross Signing", function () {
|
||||
};
|
||||
await alice.crypto!.signObject(aliceDevice);
|
||||
|
||||
const bobOlmAccount = new global.Olm.Account();
|
||||
const bobOlmAccount = new globalThis.Olm.Account();
|
||||
bobOlmAccount.create();
|
||||
const bobKeys = JSON.parse(bobOlmAccount.identity_keys());
|
||||
const bobDeviceUnsigned = {
|
||||
@@ -624,10 +622,10 @@ describe("Cross Signing", function () {
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
// (NOTE: device key is not signed by ssk)
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobSigning = new globalThis.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK: CrossSigningKeyInfo = {
|
||||
@@ -690,10 +688,10 @@ describe("Cross Signing", function () {
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's keys
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobSigning = new globalThis.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK: CrossSigningKeyInfo = {
|
||||
@@ -757,10 +755,10 @@ describe("Cross Signing", function () {
|
||||
expect(bobDeviceTrust.isTofu()).toBeTruthy();
|
||||
|
||||
// Alice downloads new SSK for Bob
|
||||
const bobMasterSigning2 = new global.Olm.PkSigning();
|
||||
const bobMasterSigning2 = new globalThis.Olm.PkSigning();
|
||||
const bobMasterPrivkey2 = bobMasterSigning2.generate_seed();
|
||||
const bobMasterPubkey2 = bobMasterSigning2.init_with_seed(bobMasterPrivkey2);
|
||||
const bobSigning2 = new global.Olm.PkSigning();
|
||||
const bobSigning2 = new globalThis.Olm.PkSigning();
|
||||
const bobPrivkey2 = bobSigning2.generate_seed();
|
||||
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
|
||||
const bobSSK2: CrossSigningKeyInfo = {
|
||||
@@ -827,7 +825,7 @@ describe("Cross Signing", function () {
|
||||
});
|
||||
|
||||
it("should offer to upgrade device verifications to cross-signing", async function () {
|
||||
let upgradeResolveFunc: Function;
|
||||
let upgradeResolveFunc: () => void;
|
||||
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
@@ -866,7 +864,7 @@ describe("Cross Signing", function () {
|
||||
// cross-signing key is signed by his Dynabook, which alice has
|
||||
// verified, and ask if the device verification should be upgraded to a
|
||||
// cross-signing verification
|
||||
let upgradePromise = new Promise((resolve) => {
|
||||
let upgradePromise = new Promise<void>((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
await resetCrossSigningKeys(alice);
|
||||
@@ -885,7 +883,7 @@ describe("Cross Signing", function () {
|
||||
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
|
||||
expect(bobTrust2.isTofu()).toBeTruthy();
|
||||
|
||||
upgradePromise = new Promise((resolve) => {
|
||||
upgradePromise = new Promise<void>((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
alice.crypto!.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
|
||||
@@ -907,10 +905,10 @@ describe("Cross Signing", function () {
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
// Generate Alice's SSK etc
|
||||
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||
const aliceMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const aliceSigning = new globalThis.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: CrossSigningKeyInfo = {
|
||||
@@ -982,10 +980,10 @@ describe("Cross Signing", function () {
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
// Generate Alice's SSK etc
|
||||
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||
const aliceMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const aliceSigning = new globalThis.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: CrossSigningKeyInfo = {
|
||||
@@ -1042,10 +1040,10 @@ describe("Cross Signing", function () {
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
// Generate Alice's SSK etc
|
||||
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||
const aliceMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const aliceSigning = new globalThis.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: CrossSigningKeyInfo = {
|
||||
@@ -1090,12 +1088,12 @@ describe("Cross Signing", function () {
|
||||
});
|
||||
|
||||
describe("userHasCrossSigningKeys", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function resetCrossSigningKeys(
|
||||
}
|
||||
|
||||
export async function createSecretStorageKey(): Promise<IRecoveryKey> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const decryption = new globalThis.Olm.PkDecryption();
|
||||
decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
decryption.free();
|
||||
|
||||
@@ -19,16 +19,16 @@ import { TestClient } from "../../TestClient";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { DEHYDRATION_ALGORITHM } from "../../../src/crypto/dehydration";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
describe("Dehydration", () => {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running dehydration unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
it("should rehydrate a dehydrated device", async () => {
|
||||
|
||||
@@ -51,7 +51,7 @@ const requests = [
|
||||
];
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
|
||||
["MemoryCryptoStore", () => new MemoryCryptoStore()],
|
||||
])("Outgoing room key requests [%s]", function (name, dbFactory) {
|
||||
|
||||
@@ -69,20 +69,20 @@ function sign<T extends IObject | ICurve25519AuthData>(
|
||||
}
|
||||
|
||||
describe("Secrets", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
it("should store and retrieve a secret", async function () {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const signing = new globalThis.Olm.PkSigning();
|
||||
const signingKey = signing.generate_seed();
|
||||
const signingPubKey = signing.init_with_seed(signingKey);
|
||||
|
||||
@@ -332,7 +332,7 @@ describe("Secrets", function () {
|
||||
});
|
||||
|
||||
it("bootstraps when cross-signing keys in secret storage", async function () {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const decryption = new globalThis.Olm.PkDecryption();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
|
||||
const bob: MatrixClient = await makeTestClient(
|
||||
|
||||
@@ -20,7 +20,7 @@ import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from
|
||||
import { CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base";
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
|
||||
["MemoryCryptoStore", () => new MemoryCryptoStore()],
|
||||
])("CryptoStore tests for %s", function (name, dbFactory) {
|
||||
|
||||
@@ -27,21 +27,21 @@ describe("IndexedDBCryptoStore", () => {
|
||||
|
||||
it("Should be true if there is a legacy database", async () => {
|
||||
// should detect a store that is not migrated
|
||||
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
|
||||
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
|
||||
await store.startup();
|
||||
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("Should be true if there is a legacy database in non migrated state", async () => {
|
||||
// should detect a store that is not migrated
|
||||
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
|
||||
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
|
||||
await store.startup();
|
||||
await store.setMigrationState(MigrationState.NOT_STARTED);
|
||||
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -54,18 +54,18 @@ describe("IndexedDBCryptoStore", () => {
|
||||
])("Exists and Migration state is %s", (migrationState) => {
|
||||
it("Should be false if migration has started", async () => {
|
||||
// should detect a store that is not migrated
|
||||
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
|
||||
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
|
||||
await store.startup();
|
||||
await store.setMigrationState(migrationState);
|
||||
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("Should be false if there is no legacy database", async () => {
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
@@ -17,10 +17,10 @@ limitations under the License.
|
||||
import "../../../olm-loader";
|
||||
import { logger } from "../../../../src/logger";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
describe("QR code verification", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running device verification tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ import { logger } from "../../../../src/logger";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { makeTestClients } from "./util";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request integration tests with crypto layer", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running device verification unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ import { MatrixClient } from "../../../../src";
|
||||
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
|
||||
import { TestClient } from "../../../TestClient";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
let ALICE_DEVICES: Record<string, IDevice>;
|
||||
let BOB_DEVICES: Record<string, IDevice>;
|
||||
|
||||
describe("SAS verification", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running device verification unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
@@ -454,7 +454,7 @@ describe("SAS verification", function () {
|
||||
let bobSasEvent: ISasEvent | null;
|
||||
let aliceVerifier: SAS;
|
||||
let bobPromise: Promise<void>;
|
||||
let clearTestClientTimeouts: Function;
|
||||
let clearTestClientTimeouts: () => void;
|
||||
|
||||
beforeEach(async function () {
|
||||
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
|
||||
@@ -34,7 +34,7 @@ const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("self-verifications", () => {
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
it("triggers a request for key sharing upon completion", async () => {
|
||||
|
||||
@@ -29,12 +29,12 @@ describe("sha256", () => {
|
||||
});
|
||||
|
||||
it("throws if webcrypto is not available", async () => {
|
||||
const oldCrypto = global.crypto;
|
||||
const oldCrypto = globalThis.crypto;
|
||||
try {
|
||||
global.crypto = {} as any;
|
||||
globalThis.crypto = {} as any;
|
||||
await expect(sha256("test")).rejects.toThrow();
|
||||
} finally {
|
||||
global.crypto = oldCrypto;
|
||||
globalThis.crypto = oldCrypto;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+258
-4
@@ -30,15 +30,18 @@ import {
|
||||
ITurnServer,
|
||||
IRoomEvent,
|
||||
IOpenIDCredentials,
|
||||
ISendEventFromWidgetResponseData,
|
||||
WidgetApiResponseError,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
|
||||
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { ICapabilities } from "../../src/embedded";
|
||||
import { ICapabilities, RoomWidgetClient } from "../../src/embedded";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { sleep } from "../../src/utils";
|
||||
|
||||
const testOIDCToken = {
|
||||
access_token: "12345678",
|
||||
@@ -90,7 +93,11 @@ class MockWidgetApi extends EventEmitter {
|
||||
public getTurnServers = jest.fn(() => []);
|
||||
public sendContentLoaded = jest.fn();
|
||||
|
||||
public transport = { reply: jest.fn() };
|
||||
public transport = {
|
||||
reply: jest.fn(),
|
||||
send: jest.fn(),
|
||||
sendComplete: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
declare module "../../src/types" {
|
||||
@@ -122,9 +129,16 @@ describe("RoomWidgetClient", () => {
|
||||
const makeClient = async (
|
||||
capabilities: ICapabilities,
|
||||
sendContentLoaded: boolean | undefined = undefined,
|
||||
userId?: string,
|
||||
): Promise<void> => {
|
||||
const baseUrl = "https://example.org";
|
||||
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl }, sendContentLoaded);
|
||||
client = createRoomWidgetClient(
|
||||
widgetApi,
|
||||
capabilities,
|
||||
"!1:example.org",
|
||||
{ baseUrl, userId },
|
||||
sendContentLoaded,
|
||||
);
|
||||
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
|
||||
widgetApi.emit("ready");
|
||||
await client.startClient();
|
||||
@@ -187,6 +201,182 @@ describe("RoomWidgetClient", () => {
|
||||
.map((e) => e.getEffectiveEvent()),
|
||||
).toEqual([event]);
|
||||
});
|
||||
describe("local echos", () => {
|
||||
const setupRemoteEcho = () => {
|
||||
makeClient(
|
||||
{
|
||||
receiveEvent: ["org.matrix.rageshake_request"],
|
||||
sendEvent: ["org.matrix.rageshake_request"],
|
||||
},
|
||||
undefined,
|
||||
"@me:example.org",
|
||||
);
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
|
||||
const injectSpy = jest.spyOn((client as any).syncApi, "injectRoomEvents");
|
||||
const widgetSendEmitter = new EventEmitter();
|
||||
const widgetSendPromise = new Promise<void>((resolve) =>
|
||||
widgetSendEmitter.once("send", () => resolve()),
|
||||
);
|
||||
const resolveWidgetSend = () => widgetSendEmitter.emit("send");
|
||||
widgetApi.sendRoomEvent.mockImplementation(
|
||||
async (eType, content, roomId): Promise<ISendEventFromWidgetResponseData> => {
|
||||
await widgetSendPromise;
|
||||
return { room_id: "!1:example.org", event_id: "event_id" };
|
||||
},
|
||||
);
|
||||
return { injectSpy, resolveWidgetSend };
|
||||
};
|
||||
const remoteEchoEvent = new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
|
||||
detail: {
|
||||
data: {
|
||||
type: "org.matrix.rageshake_request",
|
||||
|
||||
room_id: "!1:example.org",
|
||||
event_id: "event_id",
|
||||
sender: "@me:example.org",
|
||||
state_key: "bar",
|
||||
content: { hello: "world" },
|
||||
unsigned: { transaction_id: "1234" },
|
||||
},
|
||||
},
|
||||
cancelable: true,
|
||||
});
|
||||
it("get response then local echo", async () => {
|
||||
await sleep(600);
|
||||
const { injectSpy, resolveWidgetSend } = await setupRemoteEcho();
|
||||
|
||||
// Begin by sending an event:
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 12 }, "widgetTxId");
|
||||
// we do not expect it to be send -- until we call `resolveWidgetSend`
|
||||
expect(injectSpy).not.toHaveBeenCalled();
|
||||
|
||||
// We first get the response from the widget
|
||||
resolveWidgetSend();
|
||||
// We then get the remote echo from the widget
|
||||
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, remoteEchoEvent);
|
||||
|
||||
// gets emitted after the event got injected
|
||||
await new Promise<void>((resolve) => client.once(ClientEvent.Event, () => resolve()));
|
||||
expect(injectSpy).toHaveBeenCalled();
|
||||
|
||||
const call = injectSpy.mock.calls[0] as any;
|
||||
const injectedEv = call[2][0];
|
||||
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
|
||||
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
|
||||
});
|
||||
|
||||
it("get local echo then response", async () => {
|
||||
await sleep(600);
|
||||
const { injectSpy, resolveWidgetSend } = await setupRemoteEcho();
|
||||
|
||||
// Begin by sending an event:
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 12 }, "widgetTxId");
|
||||
// we do not expect it to be send -- until we call `resolveWidgetSend`
|
||||
expect(injectSpy).not.toHaveBeenCalled();
|
||||
|
||||
// We first get the remote echo from the widget
|
||||
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, remoteEchoEvent);
|
||||
expect(injectSpy).not.toHaveBeenCalled();
|
||||
|
||||
// We then get the response from the widget
|
||||
resolveWidgetSend();
|
||||
|
||||
// Gets emitted after the event got injected
|
||||
await new Promise<void>((resolve) => client.once(ClientEvent.Event, () => resolve()));
|
||||
expect(injectSpy).toHaveBeenCalled();
|
||||
|
||||
const call = injectSpy.mock.calls[0] as any;
|
||||
const injectedEv = call[2][0];
|
||||
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
|
||||
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
|
||||
});
|
||||
it("__ local echo then response", async () => {
|
||||
await sleep(600);
|
||||
const { injectSpy, resolveWidgetSend } = await setupRemoteEcho();
|
||||
|
||||
// Begin by sending an event:
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 12 }, "widgetTxId");
|
||||
// we do not expect it to be send -- until we call `resolveWidgetSend`
|
||||
expect(injectSpy).not.toHaveBeenCalled();
|
||||
|
||||
// We first get the remote echo from the widget
|
||||
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, remoteEchoEvent);
|
||||
const otherRemoteEcho = new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
|
||||
detail: { data: { ...remoteEchoEvent.detail.data } },
|
||||
});
|
||||
otherRemoteEcho.detail.data.unsigned.transaction_id = "4567";
|
||||
otherRemoteEcho.detail.data.event_id = "other_id";
|
||||
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, otherRemoteEcho);
|
||||
|
||||
// Simulate the wait time while the widget is waiting for a response
|
||||
// after we already received the remote echo
|
||||
await sleep(20);
|
||||
// even after the wait we do not want any event to be injected.
|
||||
// we do not know their event_id and cannot know if they are the remote echo
|
||||
// where we need to update the txId because they are send by this client
|
||||
expect(injectSpy).not.toHaveBeenCalled();
|
||||
// We then get the response from the widget
|
||||
resolveWidgetSend();
|
||||
|
||||
// Gets emitted after the event got injected
|
||||
await new Promise<void>((resolve) => client.once(ClientEvent.Event, () => resolve()));
|
||||
// Now we want both events to be injected since we know the txId - event_id match
|
||||
expect(injectSpy).toHaveBeenCalled();
|
||||
|
||||
// it has been called with the event sent by ourselves
|
||||
const call = injectSpy.mock.calls[0] as any;
|
||||
const injectedEv = call[2][0];
|
||||
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
|
||||
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
|
||||
|
||||
// It has been called by the event we blocked because of our send right afterwards
|
||||
const call2 = injectSpy.mock.calls[1] as any;
|
||||
const injectedEv2 = call2[2][0];
|
||||
expect(injectedEv2.getType()).toBe("org.matrix.rageshake_request");
|
||||
expect(injectedEv2.getUnsigned().transaction_id).toBe("4567");
|
||||
});
|
||||
});
|
||||
|
||||
it("handles widget errors with generic error data", async () => {
|
||||
const error = new Error("failed to send");
|
||||
widgetApi.transport.send.mockRejectedValue(error);
|
||||
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
|
||||
|
||||
await expect(
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||
).rejects.toThrow(error);
|
||||
});
|
||||
|
||||
it("handles widget errors with Matrix API error response data", async () => {
|
||||
const errorStatusCode = 400;
|
||||
const errorUrl = "http://example.org";
|
||||
const errorData = {
|
||||
errcode: "M_BAD_JSON",
|
||||
error: "Invalid body",
|
||||
};
|
||||
|
||||
const widgetError = new WidgetApiResponseError("failed to send", {
|
||||
matrix_api_error: {
|
||||
http_status: errorStatusCode,
|
||||
http_headers: {},
|
||||
url: errorUrl,
|
||||
response: errorData,
|
||||
},
|
||||
});
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
|
||||
|
||||
widgetApi.transport.send.mockRejectedValue(widgetError);
|
||||
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
|
||||
|
||||
await expect(
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||
).rejects.toThrow(matrixError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delayed events", () => {
|
||||
@@ -493,6 +683,23 @@ describe("RoomWidgetClient", () => {
|
||||
["@bob:example.org"]: { ["bobDesktop"]: { hello: "bob!" } },
|
||||
};
|
||||
|
||||
const encryptedContentMap = new Map<string, Map<string, object>>([
|
||||
["@alice:example.org", new Map([["aliceMobile", { hello: "alice!" }]])],
|
||||
["@bob:example.org", new Map([["bobDesktop", { hello: "bob!" }]])],
|
||||
]);
|
||||
|
||||
it("sends unencrypted (sendToDeviceViaWidgetApi)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
await (client as RoomWidgetClient).sendToDeviceViaWidgetApi(
|
||||
"org.example.foo",
|
||||
false,
|
||||
unencryptedContentMap,
|
||||
);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
|
||||
});
|
||||
|
||||
it("sends unencrypted (sendToDevice)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
@@ -534,6 +741,17 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sends encrypted (sendToDeviceViaWidgetApi)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
await (client as RoomWidgetClient).sendToDeviceViaWidgetApi("org.example.foo", true, encryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", true, {
|
||||
"@alice:example.org": { aliceMobile: { hello: "alice!" } },
|
||||
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ encrypted: false, title: "unencrypted" },
|
||||
{ encrypted: true, title: "encrypted" },
|
||||
@@ -570,6 +788,42 @@ describe("RoomWidgetClient", () => {
|
||||
await makeClient({});
|
||||
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
|
||||
});
|
||||
|
||||
it("handles widget errors with generic error data", async () => {
|
||||
const error = new Error("failed to get token");
|
||||
widgetApi.transport.sendComplete.mockRejectedValue(error);
|
||||
|
||||
await makeClient({});
|
||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||
|
||||
await expect(client.getOpenIdToken()).rejects.toThrow(error);
|
||||
});
|
||||
|
||||
it("handles widget errors with Matrix API error response data", async () => {
|
||||
const errorStatusCode = 400;
|
||||
const errorUrl = "http://example.org";
|
||||
const errorData = {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Bad request",
|
||||
};
|
||||
|
||||
const widgetError = new WidgetApiResponseError("failed to get token", {
|
||||
matrix_api_error: {
|
||||
http_status: errorStatusCode,
|
||||
http_headers: {},
|
||||
url: errorUrl,
|
||||
response: errorData,
|
||||
},
|
||||
});
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
|
||||
|
||||
widgetApi.transport.sendComplete.mockRejectedValue(widgetError);
|
||||
|
||||
await makeClient({});
|
||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||
|
||||
await expect(client.getOpenIdToken()).rejects.toThrow(matrixError);
|
||||
});
|
||||
});
|
||||
|
||||
it("gets TURN servers", async () => {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixError } from "../../../src";
|
||||
|
||||
type IErrorJson = MatrixError["data"];
|
||||
|
||||
describe("MatrixError", () => {
|
||||
let headers: Headers;
|
||||
|
||||
beforeEach(() => {
|
||||
headers = new Headers({ "Content-Type": "application/json" });
|
||||
});
|
||||
|
||||
function makeMatrixError(httpStatus: number, data: IErrorJson, url?: string): MatrixError {
|
||||
return new MatrixError(data, httpStatus, url, undefined, headers);
|
||||
}
|
||||
|
||||
it("should accept absent retry time from rate-limit error", () => {
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
expect(err.getRetryAfterMs()).toEqual(null);
|
||||
});
|
||||
|
||||
it("should retrieve retry_after_ms from rate-limit error", () => {
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
expect(err.getRetryAfterMs()).toEqual(150000);
|
||||
});
|
||||
|
||||
it("should ignore retry_after_ms if errcode is not M_LIMIT_EXCEEDED", () => {
|
||||
const err = makeMatrixError(429, { errcode: "M_UNKNOWN", retry_after_ms: 150000 });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
expect(err.getRetryAfterMs()).toEqual(null);
|
||||
});
|
||||
|
||||
it("should retrieve numeric Retry-After header from rate-limit error", () => {
|
||||
headers.set("Retry-After", "120");
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
// prefer Retry-After header over retry_after_ms
|
||||
expect(err.getRetryAfterMs()).toEqual(120000);
|
||||
});
|
||||
|
||||
it("should retrieve Date Retry-After header from rate-limit error", () => {
|
||||
headers.set("Retry-After", `${new Date(160000).toUTCString()}`);
|
||||
jest.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
// prefer Retry-After header over retry_after_ms
|
||||
expect(err.getRetryAfterMs()).toEqual(60000);
|
||||
});
|
||||
|
||||
it("should prefer M_FORBIDDEN errcode over HTTP status code 429", () => {
|
||||
headers.set("Retry-After", "120");
|
||||
const err = makeMatrixError(429, { errcode: "M_FORBIDDEN" });
|
||||
expect(err.isRateLimitError()).toBe(false);
|
||||
// retrieve Retry-After header even for non-M_LIMIT_EXCEEDED errors
|
||||
expect(err.getRetryAfterMs()).toEqual(120000);
|
||||
});
|
||||
|
||||
it("should prefer M_LIMIT_EXCEEDED errcode over HTTP status code 400", () => {
|
||||
headers.set("Retry-After", "120");
|
||||
const err = makeMatrixError(400, { errcode: "M_LIMIT_EXCEEDED" });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
// retrieve Retry-After header even for non-429 errors
|
||||
expect(err.getRetryAfterMs()).toEqual(120000);
|
||||
});
|
||||
|
||||
it("should reject invalid Retry-After header", () => {
|
||||
for (const invalidValue of ["-1", "1.23", new Date(0).toString()]) {
|
||||
headers.set("Retry-After", invalidValue);
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
|
||||
expect(() => err.getRetryAfterMs()).toThrow(
|
||||
"value is not a valid HTTP-date or non-negative decimal integer",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("should reject too-large Retry-After header", () => {
|
||||
headers.set("Retry-After", "1" + Array(500).fill("0").join(""));
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
|
||||
expect(() => err.getRetryAfterMs()).toThrow("integer value is too large");
|
||||
});
|
||||
|
||||
describe("can be converted to data compatible with the widget api", () => {
|
||||
it("from default values", () => {
|
||||
const matrixError = new MatrixError();
|
||||
|
||||
const widgetApiErrorData = {
|
||||
http_status: 400,
|
||||
http_headers: {},
|
||||
url: "",
|
||||
response: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown message",
|
||||
},
|
||||
};
|
||||
|
||||
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
|
||||
});
|
||||
|
||||
it("from non-default values", () => {
|
||||
headers.set("Retry-After", "120");
|
||||
const statusCode = 429;
|
||||
const data = {
|
||||
errcode: "M_LIMIT_EXCEEDED",
|
||||
error: "Request is rate-limited.",
|
||||
retry_after_ms: 120000,
|
||||
};
|
||||
const url = "http://example.net";
|
||||
|
||||
const matrixError = makeMatrixError(statusCode, data, url);
|
||||
|
||||
const widgetApiErrorData = {
|
||||
http_status: statusCode,
|
||||
http_headers: {
|
||||
"content-type": "application/json",
|
||||
"retry-after": "120",
|
||||
},
|
||||
url,
|
||||
response: data,
|
||||
};
|
||||
|
||||
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
|
||||
});
|
||||
});
|
||||
|
||||
describe("can be created from data received from the widget api", () => {
|
||||
it("from minimal data", () => {
|
||||
const statusCode = 400;
|
||||
const data = {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Something went wrong.",
|
||||
};
|
||||
const url = "";
|
||||
|
||||
const widgetApiErrorData = {
|
||||
http_status: statusCode,
|
||||
http_headers: {},
|
||||
url,
|
||||
response: data,
|
||||
};
|
||||
|
||||
headers.delete("Content-Type");
|
||||
const matrixError = makeMatrixError(statusCode, data, url);
|
||||
|
||||
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
|
||||
});
|
||||
|
||||
it("from more data", () => {
|
||||
const statusCode = 429;
|
||||
const data = {
|
||||
errcode: "M_LIMIT_EXCEEDED",
|
||||
error: "Request is rate-limited.",
|
||||
retry_after_ms: 120000,
|
||||
};
|
||||
const url = "http://example.net";
|
||||
|
||||
const widgetApiErrorData = {
|
||||
http_status: statusCode,
|
||||
http_headers: {
|
||||
"content-type": "application/json",
|
||||
"retry-after": "120",
|
||||
},
|
||||
url,
|
||||
response: data,
|
||||
};
|
||||
|
||||
headers.set("Retry-After", "120");
|
||||
const matrixError = makeMatrixError(statusCode, data, url);
|
||||
|
||||
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -60,11 +60,11 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should fall back to global fetch if fetchFn not provided", () => {
|
||||
global.fetch = jest.fn();
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
globalThis.fetch = jest.fn();
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
api.fetch("test");
|
||||
expect(global.fetch).toHaveBeenCalled();
|
||||
expect(globalThis.fetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update identity server base url", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -41,12 +41,13 @@ describe("MatrixHttpApi", () => {
|
||||
setRequestHeader: jest.fn(),
|
||||
onreadystatechange: undefined,
|
||||
getResponseHeader: jest.fn(),
|
||||
getAllResponseHeaders: jest.fn(),
|
||||
} as unknown as XMLHttpRequest;
|
||||
// We stub out XHR here as it is not available in JSDOM
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
|
||||
globalThis.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest.DONE = DONE;
|
||||
globalThis.XMLHttpRequest.DONE = DONE;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -59,7 +60,7 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should fall back to `fetch` where xhr is unavailable", () => {
|
||||
global.XMLHttpRequest = undefined!;
|
||||
globalThis.XMLHttpRequest = undefined!;
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
upload = api.uploadContent({} as File);
|
||||
@@ -171,7 +172,10 @@ describe("MatrixHttpApi", () => {
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
|
||||
xhr.status = 404;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
mocked(xhr.getResponseHeader).mockImplementation((name) =>
|
||||
name.toLowerCase() === "content-type" ? "application/json" : null,
|
||||
);
|
||||
mocked(xhr.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -86,13 +86,28 @@ describe("anySignal", () => {
|
||||
});
|
||||
|
||||
describe("parseErrorResponse", () => {
|
||||
let headers: Headers;
|
||||
const xhrHeaderMethods = {
|
||||
getResponseHeader: (name: string) => headers.get(name),
|
||||
getAllResponseHeaders: () => {
|
||||
let allHeaders = "";
|
||||
headers.forEach((value, key) => {
|
||||
allHeaders += `${key.toLowerCase()}: ${value}\r\n`;
|
||||
});
|
||||
return allHeaders;
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
headers = new Headers();
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from XHR", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
getResponseHeader(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
...xhrHeaderMethods,
|
||||
status: 500,
|
||||
} as XMLHttpRequest,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -108,14 +123,11 @@ describe("parseErrorResponse", () => {
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from fetch", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -131,13 +143,12 @@ describe("parseErrorResponse", () => {
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from XHR with urls", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
responseURL: "https://example.com",
|
||||
getResponseHeader(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
...xhrHeaderMethods,
|
||||
status: 500,
|
||||
} as XMLHttpRequest,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -154,15 +165,12 @@ describe("parseErrorResponse", () => {
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from fetch with urls", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
url: "https://example.com",
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -178,6 +186,66 @@ describe("parseErrorResponse", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("with HTTP headers", () => {
|
||||
function addHeaders(headers: Headers) {
|
||||
headers.set("Age", "0");
|
||||
headers.set("Date", "Thu, 01 Jan 1970 00:00:00 GMT"); // value contains colons
|
||||
headers.set("x-empty", "");
|
||||
headers.set("x-multi", "1");
|
||||
headers.append("x-multi", "2");
|
||||
}
|
||||
|
||||
function compareHeaders(expectedHeaders: Headers, otherHeaders: Headers | undefined) {
|
||||
expect(new Map(otherHeaders)).toEqual(new Map(expectedHeaders));
|
||||
}
|
||||
|
||||
it("should resolve HTTP Errors from XHR with headers", () => {
|
||||
headers.set("Content-Type", "text/plain");
|
||||
addHeaders(headers);
|
||||
const err = parseErrorResponse({
|
||||
...xhrHeaderMethods,
|
||||
status: 500,
|
||||
} as XMLHttpRequest) as HTTPError;
|
||||
compareHeaders(headers, err.httpHeaders);
|
||||
});
|
||||
|
||||
it("should resolve HTTP Errors from fetch with headers", () => {
|
||||
headers.set("Content-Type", "text/plain");
|
||||
addHeaders(headers);
|
||||
const err = parseErrorResponse({
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response) as HTTPError;
|
||||
compareHeaders(headers, err.httpHeaders);
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from XHR with headers", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
addHeaders(headers);
|
||||
const err = parseErrorResponse(
|
||||
{
|
||||
...xhrHeaderMethods,
|
||||
status: 500,
|
||||
} as XMLHttpRequest,
|
||||
'{"errcode": "TEST"}',
|
||||
) as MatrixError;
|
||||
compareHeaders(headers, err.httpHeaders);
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from fetch with headers", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
addHeaders(headers);
|
||||
const err = parseErrorResponse(
|
||||
{
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
) as MatrixError;
|
||||
compareHeaders(headers, err.httpHeaders);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set a sensible default error message on MatrixError", () => {
|
||||
let err = new MatrixError();
|
||||
expect(err.message).toEqual("MatrixError: Unknown message");
|
||||
@@ -188,14 +256,11 @@ describe("parseErrorResponse", () => {
|
||||
});
|
||||
|
||||
it("should handle no type gracefully", () => {
|
||||
// No Content-Type header
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -203,31 +268,38 @@ describe("parseErrorResponse", () => {
|
||||
).toStrictEqual(new HTTPError("Server returned 500 error", 500));
|
||||
});
|
||||
|
||||
it("should handle invalid type gracefully", () => {
|
||||
it("should handle empty type gracefully", () => {
|
||||
headers.set("Content-Type", " ");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? " " : null;
|
||||
},
|
||||
},
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
),
|
||||
).toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type"));
|
||||
).toStrictEqual(new Error("Error parsing Content-Type '': TypeError: argument string is required"));
|
||||
});
|
||||
|
||||
it("should handle plaintext errors", () => {
|
||||
it("should handle invalid type gracefully", () => {
|
||||
headers.set("Content-Type", "unknown");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "text/plain" : null;
|
||||
},
|
||||
},
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
),
|
||||
).toStrictEqual(new Error("Error parsing Content-Type 'unknown': TypeError: invalid media type"));
|
||||
});
|
||||
|
||||
it("should handle plaintext errors", () => {
|
||||
headers.set("Content-Type", "text/plain");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
headers,
|
||||
status: 418,
|
||||
} as Response,
|
||||
"I'm a teapot",
|
||||
|
||||
@@ -102,7 +102,7 @@ type HttpLookup = {
|
||||
error?: object;
|
||||
expectBody?: Record<string, any>;
|
||||
expectQueryParams?: QueryDict;
|
||||
thenCall?: Function;
|
||||
thenCall?: () => void;
|
||||
};
|
||||
|
||||
interface Options extends ICreateRoomOpts {
|
||||
@@ -403,7 +403,7 @@ describe("MatrixClient", function () {
|
||||
async function assertRequestsMade(
|
||||
responses: {
|
||||
prefix?: string;
|
||||
error?: { httpStatus: Number; errcode: string };
|
||||
error?: { httpStatus: number; errcode: string };
|
||||
data?: { event_id: string };
|
||||
}[],
|
||||
expectRejects = false,
|
||||
@@ -1515,7 +1515,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("emitted sync events", function () {
|
||||
function syncChecker(expectedStates: [string, string | null][], done: Function) {
|
||||
function syncChecker(expectedStates: [string, string | null][], done: () => void) {
|
||||
return function syncListener(state: SyncState, old: SyncState | null) {
|
||||
const expected = expectedStates.shift();
|
||||
logger.log("'sync' curr=%s old=%s EXPECT=%s", state, old, expected);
|
||||
@@ -1537,7 +1537,7 @@ describe("MatrixClient", function () {
|
||||
it("should transition null -> PREPARED after the first /sync", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1554,7 +1554,7 @@ describe("MatrixClient", function () {
|
||||
error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
expectedStates.push(["ERROR", null]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1594,7 +1594,7 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1605,7 +1605,7 @@ describe("MatrixClient", function () {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1630,7 +1630,7 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1649,7 +1649,7 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1664,7 +1664,7 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1695,7 +1695,7 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["ERROR", "ERROR"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -107,7 +107,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = fn;
|
||||
await tree.invite(target, false, false);
|
||||
await tree.invite(target, false);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = fn;
|
||||
await tree.invite(target, false, false);
|
||||
await tree.invite(target, false);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -133,7 +133,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
client.invite = fn;
|
||||
|
||||
await expect(tree.invite(target, false, false)).rejects.toThrow("MatrixError: Sample Failure");
|
||||
await expect(tree.invite(target, false)).rejects.toThrow("MatrixError: Sample Failure");
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -155,61 +155,10 @@ describe("MSC3089TreeSpace", () => {
|
||||
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
|
||||
];
|
||||
|
||||
await tree.invite(target, true, false);
|
||||
await tree.invite(target, true);
|
||||
expect(fn).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("should share keys with invitees", async () => {
|
||||
const target = targetUser;
|
||||
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userIds).toMatchObject([target]);
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
|
||||
client.sendSharedHistoryKeys = sendKeysFn;
|
||||
|
||||
// Mock the history check as best as possible
|
||||
const historyVis = "shared";
|
||||
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
|
||||
// We're not expecting a super rigid test: the function that calls this internally isn't
|
||||
// really being tested here.
|
||||
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
|
||||
expect(stateKey).toEqual("");
|
||||
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
|
||||
});
|
||||
room.currentState.getStateEvents = historyFn;
|
||||
|
||||
// Note: inverse test is implicit from other tests, which disable the call stack of this
|
||||
// test in order to pass.
|
||||
await tree.invite(target, false, true);
|
||||
expect(sendKeysFn).toHaveBeenCalledTimes(1);
|
||||
expect(historyFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should not share keys with invitees if inappropriate history visibility", async () => {
|
||||
const target = targetUser;
|
||||
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userIds).toMatchObject([target]);
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
|
||||
client.sendSharedHistoryKeys = sendKeysFn;
|
||||
|
||||
const historyVis = "joined"; // NOTE: Changed.
|
||||
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
|
||||
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
|
||||
expect(stateKey).toEqual("");
|
||||
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
|
||||
});
|
||||
room.currentState.getStateEvents = historyFn;
|
||||
|
||||
await tree.invite(target, false, true);
|
||||
expect(sendKeysFn).toHaveBeenCalledTimes(0);
|
||||
expect(historyFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
|
||||
makePowerLevels(pls);
|
||||
const fn = jest
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("Beacon", () => {
|
||||
|
||||
const advanceDateAndTime = (ms: number) => {
|
||||
// bc liveness check uses Date.now we have to advance this mock
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(Date.now() + ms);
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(Date.now() + ms);
|
||||
// then advance time for the interval by the same amount
|
||||
jest.advanceTimersByTime(ms);
|
||||
};
|
||||
@@ -108,11 +108,11 @@ describe("Beacon", () => {
|
||||
);
|
||||
|
||||
// back to 'now'
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(now);
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(now);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
jest.spyOn(globalThis.Date, "now").mockRestore();
|
||||
});
|
||||
|
||||
it("creates beacon from event", () => {
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("oidc authorization", () => {
|
||||
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
|
||||
mockOpenIdConfiguration(),
|
||||
);
|
||||
global.TextEncoder = TextEncoder;
|
||||
globalThis.TextEncoder = TextEncoder;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("Pushers", () => {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
store,
|
||||
});
|
||||
});
|
||||
@@ -256,7 +256,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("Read receipt", () => {
|
||||
userId: "@user:server",
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
});
|
||||
client.isGuest = () => false;
|
||||
client.supportsThreads = () => true;
|
||||
|
||||
@@ -53,8 +53,8 @@ describe("realtime-callbacks", function () {
|
||||
|
||||
it("should set 'this' to the global object", function () {
|
||||
let passed = false;
|
||||
const callback = function (this: typeof global) {
|
||||
expect(this).toBe(global); // eslint-disable-line @typescript-eslint/no-invalid-this
|
||||
const callback = function (this: typeof globalThis) {
|
||||
expect(this).toBe(globalThis); // eslint-disable-line @typescript-eslint/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line @typescript-eslint/no-invalid-this
|
||||
passed = true;
|
||||
};
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "../../olm-loader";
|
||||
import { LegacyRendezvousFailureReason as RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
|
||||
import { decodeBase64 } from "../../../src/base64";
|
||||
import { DummyTransport } from "./DummyTransport";
|
||||
|
||||
function makeTransport(name: string) {
|
||||
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "dummy" });
|
||||
}
|
||||
|
||||
describe("ECDHv2", function () {
|
||||
beforeAll(async function () {
|
||||
await global.Olm.init();
|
||||
});
|
||||
|
||||
describe("with crypto", () => {
|
||||
it("initiator wants to sign in", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
const message = { key: "xxx" };
|
||||
await alice.send(message);
|
||||
const bobReceive = await bob.receive();
|
||||
expect(bobReceive).toEqual(message);
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("initiator wants to reciprocate", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
const message = { key: "xxx" };
|
||||
await bob.send(message);
|
||||
const aliceReceive = await alice.receive();
|
||||
expect(aliceReceive).toEqual(message);
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("double connect", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
await expect(alice.connect()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("closed", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
alice.close();
|
||||
|
||||
await expect(alice.connect()).rejects.toThrow();
|
||||
await expect(alice.send({})).rejects.toThrow();
|
||||
await expect(alice.receive()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("require ciphertext", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
// send a message without encryption
|
||||
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
await expect(bob.receive()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("ciphertext before set up", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
|
||||
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await expect(alice.receive()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,712 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import "../../olm-loader";
|
||||
import {
|
||||
MSC3906Rendezvous,
|
||||
RendezvousCode,
|
||||
LegacyRendezvousFailureReason as RendezvousFailureReason,
|
||||
RendezvousIntent,
|
||||
} from "../../../src/rendezvous";
|
||||
import {
|
||||
ECDHv2RendezvousCode as ECDHRendezvousCode,
|
||||
MSC3903ECDHPayload,
|
||||
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
|
||||
} from "../../../src/rendezvous/channels";
|
||||
import { Device, MatrixClient } from "../../../src";
|
||||
import {
|
||||
MSC3886SimpleHttpRendezvousTransport,
|
||||
MSC3886SimpleHttpRendezvousTransportDetails,
|
||||
} from "../../../src/rendezvous/transports";
|
||||
import { DummyTransport } from "./DummyTransport";
|
||||
import { decodeBase64 } from "../../../src/base64";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { CrossSigningKey, OwnDeviceKeys } from "../../../src/crypto-api";
|
||||
|
||||
type UserID = string;
|
||||
type DeviceID = string;
|
||||
type Fingerprint = string;
|
||||
type SimpleDeviceMap = Record<UserID, Record<DeviceID, Fingerprint>>;
|
||||
|
||||
function mockDevice(userId: UserID, deviceId: DeviceID, fingerprint: Fingerprint): Device {
|
||||
return {
|
||||
deviceId,
|
||||
userId,
|
||||
getFingerprint: () => fingerprint,
|
||||
} as unknown as Device;
|
||||
}
|
||||
|
||||
function mockDeviceMap(
|
||||
userId: UserID,
|
||||
deviceId: DeviceID,
|
||||
deviceKey?: Fingerprint,
|
||||
otherDevices: SimpleDeviceMap = {},
|
||||
): Map<string, Map<string, Device>> {
|
||||
const deviceMap: Map<string, Map<string, Device>> = new Map();
|
||||
|
||||
const myDevices: Map<string, Device> = new Map();
|
||||
if (deviceKey) {
|
||||
myDevices.set(deviceId, mockDevice(userId, deviceId, deviceKey));
|
||||
}
|
||||
deviceMap.set(userId, myDevices);
|
||||
|
||||
for (const u in otherDevices) {
|
||||
let userDevices = deviceMap.get(u);
|
||||
if (!userDevices) {
|
||||
userDevices = new Map();
|
||||
deviceMap.set(u, userDevices);
|
||||
}
|
||||
for (const d in otherDevices[u]) {
|
||||
userDevices.set(d, mockDevice(u, d, otherDevices[u][d]));
|
||||
}
|
||||
}
|
||||
|
||||
return deviceMap;
|
||||
}
|
||||
|
||||
function makeMockClient(opts: {
|
||||
userId: UserID;
|
||||
deviceId: DeviceID;
|
||||
deviceKey?: Fingerprint;
|
||||
getLoginTokenEnabled: boolean;
|
||||
msc3882r0Only: boolean;
|
||||
msc3886Enabled: boolean;
|
||||
devices?: SimpleDeviceMap;
|
||||
verificationFunction?: (
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
verified: boolean,
|
||||
blocked: boolean,
|
||||
known: boolean,
|
||||
) => void;
|
||||
crossSigningIds?: Partial<Record<CrossSigningKey, string>>;
|
||||
}): [MatrixClient, Map<string, Map<string, Device>>] {
|
||||
const deviceMap = mockDeviceMap(opts.userId, opts.deviceId, opts.deviceKey, opts.devices);
|
||||
return [
|
||||
{
|
||||
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
|
||||
if (feature === "org.matrix.msc3886") {
|
||||
return opts.msc3886Enabled;
|
||||
} else if (feature === "org.matrix.msc3882") {
|
||||
return opts.getLoginTokenEnabled;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
getVersions() {
|
||||
return {
|
||||
unstable_features: {
|
||||
"org.matrix.msc3882": opts.getLoginTokenEnabled,
|
||||
"org.matrix.msc3886": opts.msc3886Enabled,
|
||||
},
|
||||
};
|
||||
},
|
||||
getCachedCapabilities() {
|
||||
return opts.msc3882r0Only
|
||||
? {}
|
||||
: {
|
||||
capabilities: {
|
||||
"m.get_login_token": {
|
||||
enabled: opts.getLoginTokenEnabled,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getSafeUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getDeviceId() {
|
||||
return opts.deviceId;
|
||||
},
|
||||
baseUrl: "https://example.com",
|
||||
getCrypto() {
|
||||
return {
|
||||
getUserDeviceInfo(
|
||||
[userId]: string[],
|
||||
downloadUncached?: boolean,
|
||||
): Promise<Map<string, Map<string, Device>>> {
|
||||
return Promise.resolve(deviceMap);
|
||||
},
|
||||
getCrossSigningKeyId(key: CrossSigningKey): string | null {
|
||||
return opts.crossSigningIds?.[key] ?? null;
|
||||
},
|
||||
setDeviceVerified(userId: string, deviceId: string, verified: boolean): Promise<void> {
|
||||
return Promise.resolve();
|
||||
},
|
||||
crossSignDevice(deviceId: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getOwnDeviceKeys(): Promise<OwnDeviceKeys> {
|
||||
return Promise.resolve({
|
||||
ed25519: opts.deviceKey!,
|
||||
curve25519: "aaaa",
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
} as unknown as MatrixClient,
|
||||
deviceMap,
|
||||
];
|
||||
}
|
||||
|
||||
function makeTransport(name: string, uri = "https://test.rz/123456") {
|
||||
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "http.v1", uri });
|
||||
}
|
||||
|
||||
describe("Rendezvous", function () {
|
||||
beforeAll(async function () {
|
||||
await global.Olm.init();
|
||||
});
|
||||
|
||||
let httpBackend: MockHttpBackend;
|
||||
let fetchFn: typeof global.fetch;
|
||||
let transports: DummyTransport<any, MSC3903ECDHPayload>[];
|
||||
const userId: UserID = "@user:example.com";
|
||||
|
||||
beforeEach(function () {
|
||||
httpBackend = new MockHttpBackend();
|
||||
fetchFn = httpBackend.fetchFn as typeof global.fetch;
|
||||
transports = [];
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
transports.forEach((x) => x.cleanup());
|
||||
});
|
||||
|
||||
it("generate and cancel", async function () {
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
msc3886Enabled: false,
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: true,
|
||||
});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
const aliceTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client: alice,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
|
||||
expect(aliceRz.code).toBeUndefined();
|
||||
|
||||
const codePromise = aliceRz.generateCode();
|
||||
await httpBackend.flush("");
|
||||
|
||||
await aliceRz.generateCode();
|
||||
|
||||
expect(typeof aliceRz.code).toBe("string");
|
||||
|
||||
await codePromise;
|
||||
|
||||
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
|
||||
|
||||
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
|
||||
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256");
|
||||
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
|
||||
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual(
|
||||
"https://fallbackserver/rz/123",
|
||||
);
|
||||
|
||||
httpBackend.when("DELETE", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
|
||||
const cancelPromise = aliceRz.cancel(RendezvousFailureReason.UserDeclined);
|
||||
await httpBackend.flush("");
|
||||
await expect(cancelPromise).resolves.toBeUndefined();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
|
||||
await aliceRz.close();
|
||||
});
|
||||
|
||||
async function testNoProtocols({
|
||||
getLoginTokenEnabled,
|
||||
msc3882r0Only,
|
||||
}: {
|
||||
getLoginTokenEnabled: boolean;
|
||||
msc3882r0Only: boolean;
|
||||
}) {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
msc3886Enabled: false,
|
||||
getLoginTokenEnabled,
|
||||
msc3882r0Only,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.finish",
|
||||
outcome: "unsupported",
|
||||
});
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
}
|
||||
|
||||
it("no protocols - r0", async function () {
|
||||
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: true });
|
||||
});
|
||||
|
||||
it("no protocols - stable", async function () {
|
||||
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: false });
|
||||
});
|
||||
|
||||
it("new device declines protocol with outcome unsupported", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.progress",
|
||||
protocols: ["org.matrix.msc3906.login_token"],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: "m.login.finish", outcome: "unsupported" });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
});
|
||||
|
||||
it("new device requests an invalid protocol", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.progress",
|
||||
protocols: ["org.matrix.msc3906.login_token"],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: "m.login.progress", protocol: "bad protocol" });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
});
|
||||
|
||||
it("decline on existing device", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const [alice] = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.progress",
|
||||
protocols: ["org.matrix.msc3906.login_token"],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
await aliceRz.declineLoginOnExistingDevice();
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: "m.login.finish", outcome: "declined" });
|
||||
});
|
||||
|
||||
it("approve on existing device + no verification", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const [alice] = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.progress",
|
||||
protocols: ["org.matrix.msc3906.login_token"],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
|
||||
|
||||
const bobCompleteProm = (async () => {
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: "m.login.progress", login_token: "token", homeserver: alice.baseUrl });
|
||||
await bobEcdh.send({ type: "m.login.finish", outcome: "success" });
|
||||
})();
|
||||
|
||||
await confirmProm;
|
||||
await bobCompleteProm;
|
||||
});
|
||||
|
||||
async function completeLogin(devices: SimpleDeviceMap) {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const aliceVerification = jest.fn();
|
||||
const [alice, deviceMap] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
devices,
|
||||
deviceKey: "aaaa",
|
||||
verificationFunction: aliceVerification,
|
||||
crossSigningIds: {
|
||||
master: "mmmmm",
|
||||
},
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.progress",
|
||||
protocols: ["org.matrix.msc3906.login_token"],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
|
||||
|
||||
const bobLoginProm = (async () => {
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: "m.login.progress", login_token: "token", homeserver: alice.baseUrl });
|
||||
await bobEcdh.send({ type: "m.login.finish", outcome: "success", device_id: "BOB", device_key: "bbbb" });
|
||||
})();
|
||||
|
||||
expect(await confirmProm).toEqual("BOB");
|
||||
await bobLoginProm;
|
||||
|
||||
return {
|
||||
aliceTransport,
|
||||
aliceEcdh,
|
||||
aliceRz,
|
||||
bobTransport,
|
||||
bobEcdh,
|
||||
deviceMap,
|
||||
};
|
||||
}
|
||||
|
||||
it("approve on existing device + verification", async function () {
|
||||
const { bobEcdh, aliceRz } = await completeLogin({
|
||||
[userId]: {
|
||||
BOB: "bbbb",
|
||||
},
|
||||
});
|
||||
const verifyProm = aliceRz.verifyNewDeviceOnExistingDevice();
|
||||
|
||||
const bobVerifyProm = (async () => {
|
||||
const verified = await bobEcdh.receive();
|
||||
expect(verified).toEqual({
|
||||
type: "m.login.finish",
|
||||
outcome: "verified",
|
||||
verifying_device_id: "ALICE",
|
||||
verifying_device_key: "aaaa",
|
||||
master_key: "mmmmm",
|
||||
});
|
||||
})();
|
||||
|
||||
await verifyProm;
|
||||
await bobVerifyProm;
|
||||
});
|
||||
|
||||
it("device not online within timeout", async function () {
|
||||
const { aliceRz } = await completeLogin({});
|
||||
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("device appears online within timeout", async function () {
|
||||
const devices: SimpleDeviceMap = {};
|
||||
const { aliceRz, deviceMap } = await completeLogin(devices);
|
||||
// device appears before the timeout
|
||||
setTimeout(() => {
|
||||
deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb"));
|
||||
}, 1000);
|
||||
await aliceRz.verifyNewDeviceOnExistingDevice(2000);
|
||||
});
|
||||
|
||||
it("device appears online after timeout", async function () {
|
||||
const devices: SimpleDeviceMap = {};
|
||||
const { aliceRz, deviceMap } = await completeLogin(devices);
|
||||
// device appears after the timeout
|
||||
setTimeout(() => {
|
||||
deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb"));
|
||||
}, 1500);
|
||||
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("mismatched device key", async function () {
|
||||
const { aliceRz } = await completeLogin({
|
||||
[userId]: {
|
||||
BOB: "XXXX",
|
||||
},
|
||||
});
|
||||
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
|
||||
});
|
||||
});
|
||||
@@ -1,453 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import type { MatrixClient } from "../../../src";
|
||||
import { LegacyRendezvousFailureReason as RendezvousFailureReason } from "../../../src/rendezvous";
|
||||
import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports";
|
||||
|
||||
function makeMockClient(opts: { userId: string; deviceId: string; msc3886Enabled: boolean }): MatrixClient {
|
||||
return {
|
||||
doesServerSupportUnstableFeature(feature: string) {
|
||||
return Promise.resolve(opts.msc3886Enabled && feature === "org.matrix.msc3886");
|
||||
},
|
||||
getUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getDeviceId() {
|
||||
return opts.deviceId;
|
||||
},
|
||||
requestLoginToken() {
|
||||
return Promise.resolve({ login_token: "token" });
|
||||
},
|
||||
baseUrl: "https://example.com",
|
||||
} as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
describe("SimpleHttpRendezvousTransport", function () {
|
||||
let httpBackend: MockHttpBackend;
|
||||
let fetchFn: typeof global.fetch;
|
||||
|
||||
beforeEach(function () {
|
||||
httpBackend = new MockHttpBackend();
|
||||
fetchFn = httpBackend.fetchFn as typeof global.fetch;
|
||||
});
|
||||
|
||||
async function postAndCheckLocation(
|
||||
msc3886Enabled: boolean,
|
||||
fallbackRzServer: string,
|
||||
locationResponse: string,
|
||||
expectedFinalLocation: string,
|
||||
) {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fallbackRzServer, fetchFn });
|
||||
{
|
||||
// initial POST
|
||||
const expectedPostLocation = msc3886Enabled
|
||||
? `${client.baseUrl}/_matrix/client/unstable/org.matrix.msc3886/rendezvous`
|
||||
: fallbackRzServer;
|
||||
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", expectedPostLocation).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: locationResponse,
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
await prom;
|
||||
}
|
||||
const details = await simpleHttpTransport.details();
|
||||
expect(details.uri).toBe(expectedFinalLocation);
|
||||
|
||||
{
|
||||
// first GET without etag
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", expectedFinalLocation).response = {
|
||||
body: {},
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toEqual({});
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
}
|
||||
}
|
||||
it("should throw an error when no server available", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
|
||||
await expect(simpleHttpTransport.send({})).rejects.toThrow("Invalid rendezvous URI");
|
||||
});
|
||||
|
||||
it("POST to fallback server", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("POST with no location", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await Promise.all([expect(prom).rejects.toThrow(), httpBackend.flush("")]);
|
||||
});
|
||||
|
||||
it("POST with absolute path response", async function () {
|
||||
await postAndCheckLocation(false, "https://fallbackserver/rz", "/123", "https://fallbackserver/123");
|
||||
});
|
||||
|
||||
it("POST to built-in MSC3886 implementation", async function () {
|
||||
await postAndCheckLocation(
|
||||
true,
|
||||
"https://fallbackserver/rz",
|
||||
"123",
|
||||
"https://example.com/_matrix/client/unstable/org.matrix.msc3886/rendezvous/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST with relative path response including parent", async function () {
|
||||
await postAndCheckLocation(
|
||||
false,
|
||||
"https://fallbackserver/rz/abc",
|
||||
"../xyz/123",
|
||||
"https://fallbackserver/rz/xyz/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST to follow 307 to other server", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 307,
|
||||
headers: {
|
||||
location: "https://redirected.fallbackserver/rz",
|
||||
},
|
||||
},
|
||||
};
|
||||
httpBackend.when("POST", "https://redirected.fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://redirected.fallbackserver/rz/123",
|
||||
etag: "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("POST and GET", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{
|
||||
// initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
}
|
||||
{
|
||||
// first GET without etag
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"etag": "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toEqual({ foo: "baa" });
|
||||
}
|
||||
{
|
||||
// subsequent GET which should have etag from previous request
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers }) => {
|
||||
expect(headers["if-none-match"]).toEqual("aaa");
|
||||
}).response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"etag": "bbb",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toEqual({ foo: "baa" });
|
||||
}
|
||||
});
|
||||
|
||||
it("POST and PUTs", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{
|
||||
// initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("", 1);
|
||||
await prom;
|
||||
}
|
||||
{
|
||||
// first PUT without etag
|
||||
const prom = simpleHttpTransport.send({ a: "b" });
|
||||
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers, data }) => {
|
||||
expect(headers["if-match"]).toBeUndefined();
|
||||
expect(data).toEqual({ a: "b" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 202,
|
||||
headers: {
|
||||
etag: "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("", 1);
|
||||
await prom;
|
||||
}
|
||||
{
|
||||
// subsequent PUT which should have etag from previous request
|
||||
const prom = simpleHttpTransport.send({ c: "d" });
|
||||
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => {
|
||||
expect(headers["if-match"]).toEqual("aaa");
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 202,
|
||||
headers: {
|
||||
etag: "bbb",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("", 1);
|
||||
await prom;
|
||||
}
|
||||
});
|
||||
|
||||
it("POST and DELETE", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{
|
||||
// Create
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
}
|
||||
{
|
||||
// Cancel
|
||||
const prom = simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
|
||||
httpBackend.when("DELETE", "https://fallbackserver/rz/123").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
await prom;
|
||||
}
|
||||
});
|
||||
|
||||
it("details before ready", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
await expect(simpleHttpTransport.details()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("send after cancelled", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
await simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
|
||||
await expect(simpleHttpTransport.send({})).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("receive before ready", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
await expect(simpleHttpTransport.receive()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("404 failure callback", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const onFailure = jest.fn();
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
onFailure,
|
||||
});
|
||||
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 404,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await Promise.all([
|
||||
expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined(),
|
||||
httpBackend.flush("", 1),
|
||||
]);
|
||||
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("404 failure callback mapped to expired", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const onFailure = jest.fn();
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
onFailure,
|
||||
});
|
||||
|
||||
{
|
||||
// initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
expires: "Thu, 01 Jan 1970 00:00:00 GMT",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
await prom;
|
||||
}
|
||||
{
|
||||
// GET with 404 to simulate expiry
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 404,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await Promise.all([expect(simpleHttpTransport.receive()).resolves.toBeUndefined(), httpBackend.flush("")]);
|
||||
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Expired);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -486,7 +486,43 @@ describe("RoomMember", function () {
|
||||
} as unknown as RoomState;
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).not.toEqual("Alíce"); // it should disambig.
|
||||
expect(member.name).toEqual("Alíce (@alice:bar)"); // it should disambig.
|
||||
// user_id should be there somewhere
|
||||
expect(member.name.indexOf(userA)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should disambiguate a user when their displayname looks like an MXID which isn't theirs", function () {
|
||||
const joinEvent = utils.mkMembership({
|
||||
event: true,
|
||||
mship: KnownMembership.Join,
|
||||
user: userA,
|
||||
room: roomId,
|
||||
name: "@clarissa\u0a83bar",
|
||||
});
|
||||
|
||||
const roomState = {
|
||||
getStateEvents: function (type: string) {
|
||||
if (type !== "m.room.member") {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
utils.mkMembership({
|
||||
event: true,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
user: userC,
|
||||
name: "Alice",
|
||||
}),
|
||||
joinEvent,
|
||||
];
|
||||
},
|
||||
getUserIdsWithDisplayName: function (displayName: string) {
|
||||
return [userA, userC];
|
||||
},
|
||||
} as unknown as RoomState;
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).toEqual("@clarissaઃbar (@alice:bar)"); // it should disambig.
|
||||
// user_id should be there somewhere
|
||||
expect(member.name.indexOf(userA)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("OutgoingRequestProcessor", () => {
|
||||
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||
baseUrl: "https://example.com",
|
||||
prefix: "/_matrix",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
@@ -208,7 +208,7 @@ describe("OutgoingRequestProcessor", () => {
|
||||
const outgoingRequest = new UploadSigningKeysRequest(JSON.stringify(testReq));
|
||||
|
||||
// also create a UIA callback
|
||||
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
|
||||
const authCallback: UIAuthCallback<object> = async (makeRequest) => {
|
||||
return await makeRequest({ type: "test" });
|
||||
};
|
||||
|
||||
@@ -458,7 +458,7 @@ describe("OutgoingRequestProcessor", () => {
|
||||
throw new Error("Failed to fetch");
|
||||
}
|
||||
});
|
||||
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
|
||||
const authCallback: UIAuthCallback<object> = async (makeRequest) => {
|
||||
return await makeRequest({ type: "test" });
|
||||
};
|
||||
const requestPromise = processor.makeOutgoingRequest(outgoingRequest, authCallback);
|
||||
|
||||
@@ -26,7 +26,6 @@ import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } f
|
||||
import * as TestData from "../../test-utils/test-data";
|
||||
import {
|
||||
ConnectionError,
|
||||
CryptoEvent,
|
||||
HttpApiEvent,
|
||||
HttpApiEventHandlerMap,
|
||||
IHttpOpts,
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { BackupDecryptor } from "../../../src/common-crypto/CryptoBackend";
|
||||
import { KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
|
||||
|
||||
describe("PerSessionKeyBackupDownloader", () => {
|
||||
/** The downloader under test */
|
||||
|
||||
@@ -2,13 +2,12 @@ import { Mocked } from "jest-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { CryptoEvent, HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, TypedEventEmitter } from "../../../src";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, TypedEventEmitter } from "../../../src";
|
||||
import { CryptoEvent, KeyBackupSession } from "../../../src/crypto-api/index.ts";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import * as TestData from "../../test-utils/test-data";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
|
||||
import { RustBackupManager } from "../../../src/rust-crypto/backup";
|
||||
import { RustBackupManager, KeyBackup } from "../../../src/rust-crypto/backup";
|
||||
|
||||
describe("Upload keys to backup", () => {
|
||||
/** The backup manager under test */
|
||||
@@ -26,7 +25,7 @@ describe("Upload keys to backup", () => {
|
||||
|
||||
let idGenerator = 0;
|
||||
function mockBackupRequest(keyCount: number): RustSdkCryptoJs.KeysBackupRequest {
|
||||
const requestBody: IKeyBackup = {
|
||||
const requestBody: KeyBackup = {
|
||||
rooms: {
|
||||
"!room1:server": {
|
||||
sessions: {},
|
||||
@@ -34,7 +33,7 @@ describe("Upload keys to backup", () => {
|
||||
},
|
||||
};
|
||||
for (let i = 0; i < keyCount; i++) {
|
||||
requestBody.rooms["!room1:server"].sessions["session" + i] = {} as IKeyBackupSession;
|
||||
requestBody.rooms["!room1:server"].sessions["session" + i] = {} as KeyBackupSession;
|
||||
}
|
||||
return {
|
||||
id: "id" + idGenerator++,
|
||||
|
||||
@@ -30,7 +30,6 @@ import fetchMock from "fetch-mock-jest";
|
||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../../src/rust-crypto";
|
||||
import {
|
||||
CryptoEvent,
|
||||
Device,
|
||||
DeviceVerification,
|
||||
encodeBase64,
|
||||
@@ -71,6 +70,7 @@ import { ClientEvent, ClientEventHandlerMap } from "../../../src/client";
|
||||
import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
|
||||
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
|
||||
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
|
||||
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
|
||||
|
||||
const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
@@ -1526,6 +1526,20 @@ describe("RustCrypto", () => {
|
||||
failures: 1,
|
||||
});
|
||||
});
|
||||
|
||||
describe("getKeyBackupInfo", () => {
|
||||
it("should return the current key backup info", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi());
|
||||
await expect(rustCrypto.getKeyBackupInfo()).resolves.toStrictEqual(testData.SIGNED_BACKUP_DATA);
|
||||
});
|
||||
|
||||
it("should return null if not available", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi());
|
||||
await expect(rustCrypto.getKeyBackupInfo()).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("device dehydration", () => {
|
||||
@@ -1593,6 +1607,124 @@ describe("RustCrypto", () => {
|
||||
await expect(rustCrypto.exportSecretsBundle()).resolves.toEqual(expect.objectContaining(bundle));
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptToDeviceMessages", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
let testOlmMachine: RustSdkCryptoJs.OlmMachine;
|
||||
|
||||
beforeEach(async () => {
|
||||
testOlmMachine = await OlmMachine.initialize(
|
||||
new RustSdkCryptoJs.UserId(testData.TEST_USER_ID),
|
||||
new RustSdkCryptoJs.DeviceId(testData.TEST_DEVICE_ID),
|
||||
);
|
||||
jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine);
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
expect(OlmMachine.initFromStore).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testOlmMachine?.free();
|
||||
});
|
||||
|
||||
const payload = { hello: "world" };
|
||||
|
||||
it("returns empty batch if devices not known", async () => {
|
||||
const getMissingSessions = jest.spyOn(testOlmMachine, "getMissingSessions");
|
||||
const getDevice = jest.spyOn(testOlmMachine, "getDevice");
|
||||
const batch = await rustCrypto.encryptToDeviceMessages(
|
||||
"m.test.type",
|
||||
[
|
||||
{ deviceId: "AAA", userId: "@user1:domain" },
|
||||
{ deviceId: "BBB", userId: "@user1:domain" },
|
||||
{ deviceId: "CCC", userId: "@user2:domain" },
|
||||
],
|
||||
payload,
|
||||
);
|
||||
expect(getMissingSessions.mock.calls[0][0].length).toBe(2);
|
||||
expect(getDevice).toHaveBeenCalledTimes(3);
|
||||
expect(batch?.eventType).toEqual("m.room.encrypted");
|
||||
expect(batch?.batch).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns encrypted batch for known devices", async () => {
|
||||
// Make m aware of another device, and get some OTK to be able to establish a session.
|
||||
await testOlmMachine.markRequestAsSent(
|
||||
"foo",
|
||||
RustSdkCryptoJs.RequestType.KeysQuery,
|
||||
JSON.stringify({
|
||||
device_keys: {
|
||||
"@example:localhost": {
|
||||
AFGUOBTZWM: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "AFGUOBTZWM",
|
||||
keys: {
|
||||
"curve25519:AFGUOBTZWM": "boYjDpaC+7NkECQEeMh5dC+I1+AfriX0VXG2UV7EUQo",
|
||||
"ed25519:AFGUOBTZWM": "NayrMQ33ObqMRqz6R9GosmHdT6HQ6b/RX/3QlZ2yiec",
|
||||
},
|
||||
signatures: {
|
||||
"@example:localhost": {
|
||||
"ed25519:AFGUOBTZWM":
|
||||
"RoSWvru1jj6fs2arnTedWsyIyBmKHMdOu7r9gDi0BZ61h9SbCK2zLXzuJ9ZFLao2VvA0yEd7CASCmDHDLYpXCA",
|
||||
},
|
||||
},
|
||||
user_id: "@example:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "rust-sdk",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
}),
|
||||
);
|
||||
|
||||
await testOlmMachine.markRequestAsSent(
|
||||
"bar",
|
||||
RustSdkCryptoJs.RequestType.KeysClaim,
|
||||
JSON.stringify({
|
||||
one_time_keys: {
|
||||
"@example:localhost": {
|
||||
AFGUOBTZWM: {
|
||||
"signed_curve25519:AAAABQ": {
|
||||
key: "9IGouMnkB6c6HOd4xUsNv4i3Dulb4IS96TzDordzOws",
|
||||
signatures: {
|
||||
"@example:localhost": {
|
||||
"ed25519:AFGUOBTZWM":
|
||||
"2bvUbbmJegrV0eVP/vcJKuIWC3kud+V8+C0dZtg4dVovOSJdTP/iF36tQn2bh5+rb9xLlSeztXBdhy4c+LiOAg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
}),
|
||||
);
|
||||
|
||||
const batch = await rustCrypto.encryptToDeviceMessages(
|
||||
"m.test.type",
|
||||
[
|
||||
{ deviceId: "AAA", userId: "@user1:domain" },
|
||||
{ deviceId: "BBB", userId: "@user1:domain" },
|
||||
{ deviceId: "CCC", userId: "@user2:domain" },
|
||||
{ deviceId: "AFGUOBTZWM", userId: "@example:localhost" },
|
||||
],
|
||||
payload,
|
||||
);
|
||||
expect(batch?.eventType).toEqual("m.room.encrypted");
|
||||
expect(batch?.batch.length).toEqual(1);
|
||||
expect(batch?.batch[0].deviceId).toEqual("AFGUOBTZWM");
|
||||
expect(batch?.batch[0].userId).toEqual("@example:localhost");
|
||||
expect(batch?.batch[0].payload).toEqual(
|
||||
expect.objectContaining({
|
||||
"algorithm": "m.olm.v1.curve25519-aes-sha2",
|
||||
"ciphertext": expect.any(Object),
|
||||
"org.matrix.msgid": expect.any(String),
|
||||
"sender_key": expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** Build a MatrixHttpApi instance */
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("secret-storage", () => {
|
||||
} else if (secretName == "secretE") {
|
||||
return { aaaa: {}, bbbb: {} };
|
||||
} else {
|
||||
null;
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue("aaaa"),
|
||||
|
||||
@@ -12,7 +12,7 @@ jest.useFakeTimers();
|
||||
|
||||
describe("MatrixScheduler", function () {
|
||||
let scheduler: MatrixScheduler<Record<string, boolean>>;
|
||||
let retryFn: Function | null;
|
||||
let retryFn: ((event: MatrixEvent | null, attempt: number, err: MatrixError) => number) | null;
|
||||
let queueFn: ((event: MatrixEvent) => string | null) | null;
|
||||
let deferred: IDeferred<Record<string, boolean>>;
|
||||
const roomId = "!foo:bar";
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
AccountDataClient,
|
||||
calculateKeyCheck,
|
||||
PassphraseInfo,
|
||||
SecretStorageCallbacks,
|
||||
SecretStorageKeyDescriptionAesV1,
|
||||
@@ -26,7 +27,6 @@ import {
|
||||
trimTrailingEquals,
|
||||
} from "../../src/secret-storage";
|
||||
import { randomString } from "../../src/randomstring";
|
||||
import { calculateKeyCheck } from "../../src/calculateKeyCheck.ts";
|
||||
|
||||
describe("ServerSideSecretStorageImpl", function () {
|
||||
describe(".addKey", function () {
|
||||
|
||||
@@ -794,7 +794,7 @@ describe("SyncAccumulator", function () {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
jest.spyOn(globalThis.Date, "now").mockRestore();
|
||||
});
|
||||
|
||||
it("should copy summary properties", function () {
|
||||
@@ -851,11 +851,11 @@ describe("SyncAccumulator", function () {
|
||||
const delta = 1000;
|
||||
const startingTs = 1000;
|
||||
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(startingTs);
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(startingTs);
|
||||
|
||||
sa.accumulate(RES_WITH_AGE);
|
||||
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(startingTs + delta);
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(startingTs + delta);
|
||||
|
||||
const output = sa.getJSON();
|
||||
expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned?.age).toEqual(
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
MapWithDefault,
|
||||
globToRegexp,
|
||||
escapeRegExp,
|
||||
removeHiddenChars,
|
||||
} from "../../src/utils";
|
||||
import { logger } from "../../src/logger";
|
||||
import { mkMessage } from "../test-utils/test-utils";
|
||||
@@ -740,4 +741,19 @@ describe("utils", function () {
|
||||
expect(result).toMatchInlineSnapshot(`"\\[FIT-Connect Zustelldienst \\\\\\(Testumgebung\\\\\\)\\]"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeHiddenChars", () => {
|
||||
it.each([
|
||||
["various width spaces U+2000 - U+200D", "\u2000\u200D", ""],
|
||||
["LTR and RTL marks U+200E and U+200F", "\u200E\u200F", ""],
|
||||
["LTR/RTL and other directional formatting marks U+202A - U+202F", "\u202A\u202F", ""],
|
||||
["Arabic Letter RTL mark U+061C", "\u061C", ""],
|
||||
["Combining characters U+0300 - U+036F", "\u3000\u036F", ""],
|
||||
["Zero width no-break space (BOM) U+FEFF", "\uFEFF", ""],
|
||||
["Blank/invisible characters (U2800, U2062-U2063)", "\u2800\u2062\u2063", ""],
|
||||
["Zero Width Non Joiner", "", ""],
|
||||
])("should strip invisible characters: %s", (_, input, expected) => {
|
||||
expect(removeHiddenChars(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,9 +119,9 @@ describe("Call", function () {
|
||||
const errorListener = () => {};
|
||||
|
||||
beforeEach(function () {
|
||||
prevNavigator = global.navigator;
|
||||
prevDocument = global.document;
|
||||
prevWindow = global.window;
|
||||
prevNavigator = globalThis.navigator;
|
||||
prevDocument = globalThis.document;
|
||||
prevWindow = globalThis.window;
|
||||
|
||||
installWebRTCMocks();
|
||||
|
||||
@@ -159,9 +159,9 @@ describe("Call", function () {
|
||||
call.hangup(CallErrorCode.UserHangup, true);
|
||||
|
||||
client.stop();
|
||||
global.navigator = prevNavigator;
|
||||
global.window = prevWindow;
|
||||
global.document = prevDocument;
|
||||
globalThis.navigator = prevNavigator;
|
||||
globalThis.window = prevWindow;
|
||||
globalThis.document = prevDocument;
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
@@ -788,17 +788,17 @@ describe("Call", function () {
|
||||
});
|
||||
|
||||
it("should return false if window or document are undefined", () => {
|
||||
global.window = undefined!;
|
||||
globalThis.window = undefined!;
|
||||
expect(supportsMatrixCall()).toBe(false);
|
||||
global.window = prevWindow;
|
||||
global.document = undefined!;
|
||||
globalThis.window = prevWindow;
|
||||
globalThis.document = undefined!;
|
||||
expect(supportsMatrixCall()).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if RTCPeerConnection throws", () => {
|
||||
// @ts-ignore - writing to window as we are simulating browser edge-cases
|
||||
global.window = {};
|
||||
Object.defineProperty(global.window, "RTCPeerConnection", {
|
||||
globalThis.window = {};
|
||||
Object.defineProperty(globalThis.window, "RTCPeerConnection", {
|
||||
get: () => {
|
||||
throw Error("Secure mode, naaah!");
|
||||
},
|
||||
@@ -810,11 +810,11 @@ describe("Call", function () {
|
||||
"should return false if RTCPeerConnection & RTCSessionDescription " +
|
||||
"& RTCIceCandidate & mediaDevices are unavailable",
|
||||
() => {
|
||||
global.window.RTCPeerConnection = undefined!;
|
||||
global.window.RTCSessionDescription = undefined!;
|
||||
global.window.RTCIceCandidate = undefined!;
|
||||
globalThis.window.RTCPeerConnection = undefined!;
|
||||
globalThis.window.RTCSessionDescription = undefined!;
|
||||
globalThis.window.RTCIceCandidate = undefined!;
|
||||
// @ts-ignore - writing to a read-only property as we are simulating faulty browsers
|
||||
global.navigator.mediaDevices = undefined;
|
||||
globalThis.navigator.mediaDevices = undefined;
|
||||
expect(supportsMatrixCall()).toBe(false);
|
||||
},
|
||||
);
|
||||
@@ -1228,7 +1228,7 @@ describe("Call", function () {
|
||||
});
|
||||
|
||||
describe("Screen sharing", () => {
|
||||
const waitNegotiateFunc = (resolve: Function): void => {
|
||||
const waitNegotiateFunc = (resolve: () => void): void => {
|
||||
mockSendEvent.mockImplementationOnce(() => {
|
||||
// Note that the peer connection here is a dummy one and always returns
|
||||
// dummy SDP, so there's not much point returning the content: the SDP will
|
||||
@@ -1305,7 +1305,7 @@ describe("Call", function () {
|
||||
});
|
||||
|
||||
it("removes RTX codec from screen sharing transcievers", async () => {
|
||||
mocked(global.RTCRtpSender.getCapabilities).mockReturnValue({
|
||||
mocked(globalThis.RTCRtpSender.getCapabilities).mockReturnValue({
|
||||
codecs: [
|
||||
{ mimeType: "video/rtx", clockRate: 90000 },
|
||||
{ mimeType: "video/somethingelse", clockRate: 90000 },
|
||||
@@ -1816,8 +1816,8 @@ describe("Call", function () {
|
||||
|
||||
it("should emit IceFailed error on the successor call if RTCPeerConnection throws", async () => {
|
||||
// @ts-ignore - writing to window as we are simulating browser edge-cases
|
||||
global.window = {};
|
||||
Object.defineProperty(global.window, "RTCPeerConnection", {
|
||||
globalThis.window = {};
|
||||
Object.defineProperty(globalThis.window, "RTCPeerConnection", {
|
||||
get: () => {
|
||||
throw Error("Secure mode, naaah!");
|
||||
},
|
||||
|
||||
@@ -101,7 +101,7 @@ describe("CallFeed", () => {
|
||||
it.each([
|
||||
[CallState.Connected, true],
|
||||
[CallState.Connecting, false],
|
||||
])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => {
|
||||
])("should react to call state, when !isLocal()", (state: CallState, expected: boolean) => {
|
||||
call.emit(CallEvent.State, state, CallState.InviteSent, call.typed());
|
||||
|
||||
expect(feed.connected).toBe(expected);
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("Media Handler", function () {
|
||||
beforeEach(() => {
|
||||
mockMediaDevices = new MockMediaDevices();
|
||||
|
||||
global.navigator = {
|
||||
globalThis.navigator = {
|
||||
mediaDevices: mockMediaDevices.typed(),
|
||||
} as unknown as Navigator;
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ describe("GroupCallStats", () => {
|
||||
|
||||
it("doing nothing if process already running", async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(global, "setInterval").mockReturnValue(22);
|
||||
jest.spyOn(globalThis, "setInterval").mockReturnValue(22);
|
||||
stats.start();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
stats.start();
|
||||
@@ -146,8 +146,8 @@ describe("GroupCallStats", () => {
|
||||
});
|
||||
it("finish stats process if was started", async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(global, "setInterval").mockReturnValue(22);
|
||||
jest.spyOn(global, "clearInterval");
|
||||
jest.spyOn(globalThis, "setInterval").mockReturnValue(22);
|
||||
jest.spyOn(globalThis, "clearInterval");
|
||||
stats.start();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
stats.stop();
|
||||
@@ -155,7 +155,7 @@ describe("GroupCallStats", () => {
|
||||
});
|
||||
|
||||
it("do nothing if stats process was not started", async () => {
|
||||
jest.spyOn(global, "clearInterval");
|
||||
jest.spyOn(globalThis, "clearInterval");
|
||||
stats.stop();
|
||||
expect(clearInterval).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Vendored
+2
-2
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// this is needed to tell TS about global.Olm
|
||||
// this is needed to tell TS about globalThis.Olm
|
||||
import "@matrix-org/olm";
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
// use `number` as the return type in all cases for global.set{Interval,Timeout},
|
||||
// use `number` as the return type in all cases for globalThis.set{Interval,Timeout},
|
||||
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
|
||||
// The overload for clear{Interval,Timeout} is resolved as expected.
|
||||
// We use `ReturnType<typeof setTimeout>` in the code to be agnostic of if this definition gets loaded.
|
||||
|
||||
@@ -111,18 +111,16 @@ type DelayedPartialTimelineEvent = {
|
||||
|
||||
type DelayedPartialStateEvent = DelayedPartialTimelineEvent & {
|
||||
state_key: string;
|
||||
transaction_id: string;
|
||||
};
|
||||
|
||||
type DelayedPartialEvent = DelayedPartialTimelineEvent | DelayedPartialStateEvent;
|
||||
|
||||
export type DelayedEventInfo = {
|
||||
delayed_events: DelayedPartialEvent &
|
||||
delayed_events: (DelayedPartialEvent &
|
||||
SendDelayedEventResponse &
|
||||
SendDelayedEventRequestOpts &
|
||||
{
|
||||
SendDelayedEventRequestOpts & {
|
||||
running_since: number;
|
||||
}[];
|
||||
})[];
|
||||
next_batch?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -414,16 +414,16 @@ export class AutoDiscovery {
|
||||
}
|
||||
}
|
||||
|
||||
private static fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
|
||||
private static fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof globalThis.fetch> {
|
||||
if (this.fetchFn) {
|
||||
return this.fetchFn(resource, options);
|
||||
}
|
||||
return global.fetch(resource, options);
|
||||
return globalThis.fetch(resource, options);
|
||||
}
|
||||
|
||||
private static fetchFn?: typeof global.fetch;
|
||||
private static fetchFn?: typeof globalThis.fetch;
|
||||
|
||||
public static setFetchFn(fetchFn: typeof global.fetch): void {
|
||||
public static setFetchFn(fetchFn: typeof globalThis.fetch): void {
|
||||
AutoDiscovery.fetchFn = fetchFn;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user