Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3de2b9bf80 | |||
| ded87290ce | |||
| cf39595bd7 | |||
| c90ea985c6 | |||
| c54ca29aa8 | |||
| beb3721e7a | |||
| 1cad6f4451 | |||
| c4ea57d42d | |||
| d3f5526ec0 | |||
| b8e332b53d | |||
| ab78acc7da | |||
| 051f4e2ab9 | |||
| 8863e42e35 | |||
| bc5246970c | |||
| edac6a9983 | |||
| 97ef1dc6df | |||
| 125e45c24d | |||
| 3781b6ebfa | |||
| 8fc77c595a | |||
| 5bcd26e506 | |||
| 66f099b2e7 | |||
| b1445d7457 | |||
| b3794760c6 | |||
| fbdcc597b6 | |||
| 8ac32b7894 | |||
| b27c6de78d | |||
| c8e0987774 | |||
| 849fcd3341 | |||
| 8df8266e4a | |||
| cba4edfd84 | |||
| 4f50290cd6 | |||
| 35e31f02ac | |||
| 69647a33b6 | |||
| 13df897896 | |||
| 006929ac0c | |||
| 405a6fbb92 | |||
| d6f3262e12 | |||
| 8b32f3eb7f | |||
| 1e9934a69d | |||
| a6d342d3a4 | |||
| 4b9a1bd53f | |||
| 26248f85d5 | |||
| ae2ae483db | |||
| c87692d0aa | |||
| 2dd4334e20 | |||
| 2210255d6f | |||
| 544ac86d20 | |||
| 781c3b05e5 | |||
| 325dace437 | |||
| 5c894b34b3 | |||
| f5f4091a00 | |||
| 52bdb57a47 | |||
| 3c23eb69b5 | |||
| c93b7ce188 | |||
| 705b6336cf | |||
| 7c41e3fb06 | |||
| a314e612fa | |||
| ac6cad2852 | |||
| 906390f0bf | |||
| 795497fafa | |||
| ffb777d118 | |||
| fbba8a2d71 | |||
| 053c5741b0 |
@@ -138,6 +138,7 @@ module.exports = {
|
||||
tryExtensions: [".ts"],
|
||||
},
|
||||
],
|
||||
"no-extra-boolean-cast": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Tests written for new code (and old code if feasible).
|
||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||
- [ ] Linter and other CI checks pass.
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
|
||||
- [ ] Tests written for new code (and old code if feasible).
|
||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||
- [ ] Linter and other CI checks pass.
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
|
||||
|
||||
@@ -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,6 +7,8 @@ on:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport
|
||||
|
||||
@@ -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-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' }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
@@ -44,6 +45,8 @@ jobs:
|
||||
name: Label Community PRs
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event.action == 'opened'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check membership
|
||||
if: github.event.pull_request.user.login != 'renovate[bot]'
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Release Sanity checks
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: false
|
||||
inputs:
|
||||
repository:
|
||||
type: string
|
||||
required: false
|
||||
default: ${{ github.repository }}
|
||||
description: "The repository (in form owner/repo) to check for release blockers"
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
checks:
|
||||
name: Sanity checks
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check for X-Release-Blocker label on any open issues or PRs
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
REPO: ${{ inputs.repository }}
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { REPO } = process.env;
|
||||
const { data } = await github.rest.search.issuesAndPullRequests({
|
||||
q: `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!`);
|
||||
}
|
||||
@@ -8,9 +8,12 @@ on:
|
||||
type: string
|
||||
required: false
|
||||
concurrency: release-drafter-action
|
||||
permissions: {}
|
||||
jobs:
|
||||
draft:
|
||||
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-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
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ on:
|
||||
description: Publish to npm
|
||||
type: boolean
|
||||
default: false
|
||||
downstreams:
|
||||
description: List of github projects (owner/repo) which should have their dependency bumped to the newly released version (in JSON string array string syntax)
|
||||
type: string
|
||||
required: false
|
||||
gpg-fingerprint:
|
||||
description: Fingerprint of the GPG key to use for signing the git tag and assets, if any.
|
||||
type: string
|
||||
@@ -38,16 +34,31 @@ on:
|
||||
description: The number of expected assets, including signatures, excluding generated zip & tarball.
|
||||
type: number
|
||||
required: false
|
||||
outputs:
|
||||
npm-id:
|
||||
description: "The npm package@version string we published"
|
||||
value: ${{ jobs.npm.outputs.id }}
|
||||
permissions: {}
|
||||
jobs:
|
||||
checks:
|
||||
name: Sanity checks
|
||||
permissions:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
|
||||
|
||||
release:
|
||||
name: Release
|
||||
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 +76,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
|
||||
|
||||
@@ -274,6 +286,8 @@ jobs:
|
||||
name: Post release steps
|
||||
needs: release
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- id: repository
|
||||
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||
@@ -297,34 +311,3 @@ jobs:
|
||||
# wait-interval: 10
|
||||
# check-name: merge
|
||||
# allowed-conclusions: success
|
||||
|
||||
bump-downstreams:
|
||||
name: Update npm dependency in downstream projects
|
||||
needs: npm
|
||||
runs-on: ubuntu-24.04
|
||||
if: inputs.downstreams
|
||||
strategy:
|
||||
matrix:
|
||||
repo: ${{ fromJSON(inputs.downstreams) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.npm.outputs.id }}
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
yarn upgrade "$DEPENDENCY" --exact
|
||||
git add package.json yarn.lock
|
||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||
git push origin staging
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
|
||||
@@ -21,14 +21,49 @@ 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
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: read
|
||||
secrets: inherit
|
||||
with:
|
||||
final: ${{ inputs.mode == 'final' }}
|
||||
npm: ${{ inputs.npm }}
|
||||
downstreams: '["element-hq/element-web"]'
|
||||
|
||||
bump-downstreams:
|
||||
name: Update npm dependency in downstream projects
|
||||
needs: release
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
repo:
|
||||
- element-hq/element-web
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
yarn upgrade "$DEPENDENCY" --exact
|
||||
git add package.json yarn.lock
|
||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||
git push origin staging
|
||||
|
||||
docs:
|
||||
name: Publish Documentation
|
||||
|
||||
@@ -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-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@66088c44e212a906c32a047529a213d81809ec1c
|
||||
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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # 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@66088c44e212a906c32a047529a213d81809ec1c
|
||||
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,6 +8,7 @@ on:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
permissions: {} # No permissions needed
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
@@ -101,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
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 }})"
|
||||
@@ -77,6 +78,8 @@ jobs:
|
||||
name: Downstream test element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
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: "."
|
||||
@@ -107,10 +111,12 @@ jobs:
|
||||
if: always()
|
||||
needs:
|
||||
- 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@66088c44e212a906c32a047529a213d81809ec1c
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
@@ -3,7 +3,7 @@ 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-24.04
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -126,26 +126,26 @@ const img = await fetch(downloadUrl, {
|
||||
This SDK provides a full object model around the Matrix Client-Server API and emits
|
||||
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
|
||||
|
||||
- Handles syncing (via `/sync`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
- Handles syncing (via `/sync`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
|
||||
# Usage
|
||||
|
||||
@@ -303,44 +303,131 @@ 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.initLegacyCrypto()`. 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:
|
||||
**WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for ensuring that only one `MatrixClient` issue is instantiated at a time.
|
||||
|
||||
- download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
|
||||
- load `olm.js` as a `<script>` _before_ `browser-matrix.js`.
|
||||
## Secret storage
|
||||
|
||||
To provide the Olm library in a node.js application:
|
||||
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).
|
||||
|
||||
- `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`.
|
||||
```javascript
|
||||
const matrixClient = sdk.createClient({
|
||||
...,
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: async (keys) => {
|
||||
// This function should prompt the user to enter their secret storage key.
|
||||
return mySecretStorageKeys;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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.
|
||||
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;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
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.initLegacyCrypto()`), you will
|
||||
need to migrate existing devices to the Rust crypto stack.
|
||||
|
||||
This migration happens automatically when you call `initRustCrypto()` instead of `initLegacyCrypto()`,
|
||||
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
|
||||
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
# Summary
|
||||
|
||||
- [Introduction](../README.md)
|
||||
- [Introduction](../README.md)
|
||||
|
||||
# Deep dive
|
||||
|
||||
- [Storage notes](storage-notes.md)
|
||||
- [Unverified devices](warning-on-unverified-devices.md)
|
||||
- [Storage notes](storage-notes.md)
|
||||
- [Unverified devices](warning-on-unverified-devices.md)
|
||||
|
||||
+27
-27
@@ -20,19 +20,19 @@ blurrier.
|
||||
|
||||
When we are low on disk space overall or near the group limit / origin quota:
|
||||
|
||||
- Chrome
|
||||
- Log database may fail to start with AbortError
|
||||
- IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
- When near the quota, QuotaExceededError is used more consistently
|
||||
- Firefox
|
||||
- The first error will be QuotaExceededError
|
||||
- Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
- Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
- Can revive access if you reopen the DB
|
||||
- Chrome
|
||||
- Log database may fail to start with AbortError
|
||||
- IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
- When near the quota, QuotaExceededError is used more consistently
|
||||
- Firefox
|
||||
- The first error will be QuotaExceededError
|
||||
- Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
- Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
- Can revive access if you reopen the DB
|
||||
|
||||
## Cache Eviction
|
||||
|
||||
@@ -41,9 +41,9 @@ limited by a single quota, in practice, browsers appear to handle `localStorage`
|
||||
separately from the others, so it has a separate quota limit and isn't evicted
|
||||
when low on space.
|
||||
|
||||
- Chrome, Firefox
|
||||
- IndexedDB for origin deleted
|
||||
- Local Storage remains in place
|
||||
- Chrome, Firefox
|
||||
- IndexedDB for origin deleted
|
||||
- Local Storage remains in place
|
||||
|
||||
## Persistent Storage
|
||||
|
||||
@@ -51,20 +51,20 @@ Storage Standard offers a `navigator.storage.persist` API that can be used to
|
||||
request persistent storage that won't be deleted by the browser because of low
|
||||
space.
|
||||
|
||||
- Chrome
|
||||
- Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
- Firefox
|
||||
- Firefox 67 shows a prompt to grant
|
||||
- Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
- Chrome
|
||||
- Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
- Firefox
|
||||
- Firefox 67 shows a prompt to grant
|
||||
- Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
|
||||
## Storage Estimation
|
||||
|
||||
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
|
||||
how much space remains.
|
||||
|
||||
- Chrome, Firefox
|
||||
- Can run this at any time to request an estimate of space remaining
|
||||
- Firefox
|
||||
- Returns `0` for `usage` if a site is persisted
|
||||
- Chrome, Firefox
|
||||
- Can run this at any time to request an estimate of space remaining
|
||||
- Firefox
|
||||
- Returns `0` for `usage` if a site is persisted
|
||||
|
||||
@@ -17,13 +17,13 @@ Warn when you initial sync if the room has any new undefined devices since you w
|
||||
|
||||
Warn when the user tries to send a message:
|
||||
|
||||
- If the room has unverified devices which the user has not yet been told about in the context of this room
|
||||
...or in the context of this user? currently all verification is per-user, not per-room.
|
||||
...this should be good enough.
|
||||
- If the room has unverified devices which the user has not yet been told about in the context of this room
|
||||
...or in the context of this user? currently all verification is per-user, not per-room.
|
||||
...this should be good enough.
|
||||
|
||||
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
|
||||
throw an error when trying to encrypt if there are pure unverified devices there
|
||||
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
|
||||
- or megolm could warn which devices are causing the problems.
|
||||
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
|
||||
throw an error when trying to encrypt if there are pure unverified devices there
|
||||
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
|
||||
- or megolm could warn which devices are causing the problems.
|
||||
|
||||
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "34.12.0",
|
||||
"version": "35.0.0-rc.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -50,7 +50,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^11.0.0",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
@@ -103,7 +103,7 @@
|
||||
"eslint-plugin-jsdoc": "^50.0.0",
|
||||
"eslint-plugin-matrix-org": "^2.0.1",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.3.0",
|
||||
"eslint-plugin-tsdoc": "^0.4.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "11.1.5",
|
||||
@@ -117,12 +117,12 @@
|
||||
"lint-staged": "^15.0.2",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.3.3",
|
||||
"prettier": "3.4.1",
|
||||
"rimraf": "^6.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.0",
|
||||
"typedoc": "^0.27.0",
|
||||
"typedoc-plugin-coverage": "^3.0.0",
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
"typedoc-plugin-mdn-links": "^4.0.0",
|
||||
"typedoc-plugin-missing-exports": "^3.0.0",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
|
||||
@@ -1327,7 +1327,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
const syncResponse = getSyncResponse(["@bob:xyz"]);
|
||||
// Every 2 messages in the room, the session should be rotated
|
||||
syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = {
|
||||
syncResponse.rooms[Category.Join][ROOM_ID].state!.events[0].content = {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
rotation_period_msgs: 2,
|
||||
};
|
||||
@@ -1383,7 +1383,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const oneHourInMs = 60 * 60 * 1000;
|
||||
|
||||
// Every 1h the session should be rotated
|
||||
syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = {
|
||||
syncResponse.rooms[Category.Join][ROOM_ID].state!.events[0].content = {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
rotation_period_ms: oneHourInMs,
|
||||
};
|
||||
@@ -1741,7 +1741,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
await testClient.client.initCrypto();
|
||||
await testClient.client.initLegacyCrypto();
|
||||
const keys = [
|
||||
{
|
||||
room_id: ROOM_ID,
|
||||
@@ -1853,7 +1853,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
oldBackendOnly("Alice receives shared history before being invited to a room by the sharer", async () => {
|
||||
const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux");
|
||||
await beccaTestClient.client.initCrypto();
|
||||
await beccaTestClient.client.initLegacyCrypto();
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
@@ -2007,7 +2007,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
oldBackendOnly("Alice receives shared history before being invited to a room by someone else", async () => {
|
||||
const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux");
|
||||
await beccaTestClient.client.initCrypto();
|
||||
await beccaTestClient.client.initLegacyCrypto();
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Mocked } from "jest-mock";
|
||||
import {
|
||||
createClient,
|
||||
Crypto,
|
||||
CryptoEvent,
|
||||
encodeBase64,
|
||||
ICreateClientOpts,
|
||||
IEvent,
|
||||
IMegolmSessionData,
|
||||
@@ -44,7 +44,7 @@ import * as testData from "../../test-utils/test-data";
|
||||
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||
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;
|
||||
@@ -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();
|
||||
|
||||
@@ -345,10 +345,10 @@ describe("MatrixClient crypto", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
|
||||
await aliTestClient.client.initCrypto();
|
||||
await aliTestClient.client.initLegacyCrypto();
|
||||
|
||||
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
|
||||
await bobTestClient.client.initCrypto();
|
||||
await bobTestClient.client.initLegacyCrypto();
|
||||
|
||||
aliMessages = [];
|
||||
bobMessages = [];
|
||||
|
||||
@@ -78,6 +78,7 @@ import {
|
||||
encryptGroupSessionKey,
|
||||
encryptMegolmEvent,
|
||||
encryptSecretSend,
|
||||
getTestOlmAccountKeys,
|
||||
ToDeviceEvent,
|
||||
} from "./olm-utils";
|
||||
import { KeyBackupInfo } from "../../../src/crypto-api";
|
||||
@@ -473,21 +474,23 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||
|
||||
// we should now have QR data we can display
|
||||
const qrCodeBuffer = (await request.generateQRCode())!;
|
||||
expect(qrCodeBuffer).toBeTruthy();
|
||||
const rawQrCodeBuffer = (await request.generateQRCode())!;
|
||||
expect(rawQrCodeBuffer).toBeTruthy();
|
||||
const qrCodeBuffer = new Uint8Array(rawQrCodeBuffer);
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
|
||||
expect(qrCodeBuffer.subarray(0, 6).toString("latin1")).toEqual("MATRIX");
|
||||
expect(qrCodeBuffer.readUint8(6)).toEqual(0x02); // version
|
||||
expect(qrCodeBuffer.readUint8(7)).toEqual(0x02); // mode
|
||||
const txnIdLen = qrCodeBuffer.readUint16BE(8);
|
||||
expect(qrCodeBuffer.subarray(10, 10 + txnIdLen).toString("utf-8")).toEqual(transactionId);
|
||||
expect(textDecoder.decode(qrCodeBuffer.slice(0, 6))).toEqual("MATRIX");
|
||||
expect(qrCodeBuffer[6]).toEqual(0x02); // version
|
||||
expect(qrCodeBuffer[7]).toEqual(0x02); // mode
|
||||
const txnIdLen = (qrCodeBuffer[8] << 8) + qrCodeBuffer[9];
|
||||
expect(textDecoder.decode(qrCodeBuffer.slice(10, 10 + txnIdLen))).toEqual(transactionId);
|
||||
// Alice's device's public key comes next, but we have nothing to do with it here.
|
||||
// const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen);
|
||||
expect(qrCodeBuffer.subarray(42 + txnIdLen, 32 + 42 + txnIdLen)).toEqual(
|
||||
Buffer.from(MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, "base64"),
|
||||
// const aliceDevicePubKey = qrCodeBuffer.slice(10 + txnIdLen, 32 + 10 + txnIdLen);
|
||||
expect(encodeUnpaddedBase64(qrCodeBuffer.slice(42 + txnIdLen, 32 + 42 + txnIdLen))).toEqual(
|
||||
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
|
||||
);
|
||||
const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen);
|
||||
const sharedSecret = qrCodeBuffer.slice(74 + txnIdLen);
|
||||
|
||||
// we should still be "Ready" and have no verifier
|
||||
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||
@@ -805,7 +808,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
// we should now have QR data we can display
|
||||
const qrCodeBuffer = (await request.generateQRCode())!;
|
||||
expect(qrCodeBuffer).toBeTruthy();
|
||||
const sharedSecret = qrCodeBuffer.subarray(74 + transactionId.length);
|
||||
const sharedSecret = qrCodeBuffer.slice(74 + transactionId.length);
|
||||
|
||||
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
|
||||
returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret));
|
||||
@@ -990,6 +993,18 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID]));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Rust crypto requires the sender's device keys before it accepts a
|
||||
// verification request.
|
||||
if (backend === "rust-sdk") {
|
||||
const crypto = aliceClient.getCrypto()!;
|
||||
|
||||
const bobDeviceKeys = getTestOlmAccountKeys(testOlmAccount, BOB_TEST_USER_ID, "BobDevice");
|
||||
e2eKeyResponder.addDeviceKeys(bobDeviceKeys);
|
||||
syncResponder.sendOrQueueSyncResponse({ device_lists: { changed: [BOB_TEST_USER_ID] } });
|
||||
await syncPromise(aliceClient);
|
||||
await crypto.getUserDeviceInfo([BOB_TEST_USER_ID]);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1627,7 +1642,7 @@ function buildReadyMessage(
|
||||
}
|
||||
|
||||
/** build an m.key.verification.start to-device message suitable for the m.reciprocate.v1 flow, originating from the dummy device */
|
||||
function buildReciprocateStartMessage(transactionId: string, sharedSecret: Uint8Array) {
|
||||
function buildReciprocateStartMessage(transactionId: string, sharedSecret: ArrayBuffer) {
|
||||
return {
|
||||
type: "m.key.verification.start",
|
||||
content: {
|
||||
@@ -1723,7 +1738,7 @@ function buildQRCode(
|
||||
key2Base64: string,
|
||||
sharedSecret: string,
|
||||
mode = 0x02,
|
||||
): Uint8Array {
|
||||
): Uint8ClampedArray {
|
||||
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
|
||||
|
||||
const qrCodeBuffer = Buffer.alloc(150); // oversize
|
||||
@@ -1739,5 +1754,5 @@ function buildQRCode(
|
||||
idx += qrCodeBuffer.write(sharedSecret, idx);
|
||||
|
||||
// truncate to the right length
|
||||
return qrCodeBuffer.subarray(0, idx);
|
||||
return new Uint8ClampedArray(qrCodeBuffer.subarray(0, idx));
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("DeviceList management:", function () {
|
||||
|
||||
async function createTestClient() {
|
||||
const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend);
|
||||
await testClient.client.initCrypto();
|
||||
await testClient.client.initLegacyCrypto();
|
||||
return testClient;
|
||||
}
|
||||
|
||||
|
||||
@@ -1144,7 +1144,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
// Assume we're seeing the reply while loading backlog
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
@@ -1155,7 +1155,7 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
await flushHttp(prom);
|
||||
// but while loading the metadata, a new reply has arrived
|
||||
await room.addLiveEvents([THREAD_REPLY3]);
|
||||
await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
|
||||
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
|
||||
// then the events should still be all in the right order
|
||||
expect(thread.events.map((it) => it.getId())).toEqual([
|
||||
@@ -1247,7 +1247,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
// Assume we're seeing the reply while loading backlog
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
@@ -1263,7 +1263,7 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
await flushHttp(prom);
|
||||
// but while loading the metadata, a new reply has arrived
|
||||
await room.addLiveEvents([THREAD_REPLY3]);
|
||||
await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
|
||||
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
|
||||
// then the events should still be all in the right order
|
||||
expect(thread.events.map((it) => it.getId())).toEqual([
|
||||
@@ -1560,7 +1560,7 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.NewReply);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
expect(thread.length).toBe(2);
|
||||
@@ -1685,7 +1685,7 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
await room.addLiveEvents([THREAD_REPLY_REACTION]);
|
||||
await room.addLiveEvents([THREAD_REPLY_REACTION], { addToState: false });
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
expect(thread.length).toBe(1); // reactions don't count towards the length of a thread
|
||||
|
||||
@@ -168,14 +168,17 @@ describe("MatrixClient", function () {
|
||||
type: "test",
|
||||
content: {},
|
||||
});
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
room.addLiveEvents(
|
||||
[
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
],
|
||||
{ addToState: true },
|
||||
);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
store.storeRoom(room);
|
||||
|
||||
@@ -188,14 +191,17 @@ describe("MatrixClient", function () {
|
||||
const roomId = "!roomId:server";
|
||||
const roomAlias = "#my-fancy-room:server";
|
||||
const room = new Room(roomId, client, userId);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
room.addLiveEvents(
|
||||
[
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
],
|
||||
{ addToState: true },
|
||||
);
|
||||
store.storeRoom(room);
|
||||
|
||||
// The method makes a request to resolve the alias
|
||||
@@ -275,14 +281,17 @@ describe("MatrixClient", function () {
|
||||
content: {},
|
||||
});
|
||||
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Knock,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
room.addLiveEvents(
|
||||
[
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Knock,
|
||||
event: true,
|
||||
}),
|
||||
],
|
||||
{ addToState: true },
|
||||
);
|
||||
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
store.storeRoom(room);
|
||||
@@ -641,9 +650,9 @@ describe("MatrixClient", function () {
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
// running initCrypto should trigger a key upload
|
||||
// running initLegacyCrypto should trigger a key upload
|
||||
httpBackend.when("POST", "/keys/upload").respond(200, {});
|
||||
return Promise.all([client.initCrypto(), httpBackend.flush("/keys/upload", 1)]);
|
||||
return Promise.all([client.initLegacyCrypto(), httpBackend.flush("/keys/upload", 1)]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1906,6 +1915,28 @@ describe("MatrixClient", function () {
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDomain", () => {
|
||||
it("should return null if no userId is set", () => {
|
||||
const client = new MatrixClient({ baseUrl: "http://localhost" });
|
||||
expect(client.getDomain()).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the domain of the userId", () => {
|
||||
expect(client.getDomain()).toBe("localhost");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserIdLocalpart", () => {
|
||||
it("should return null if no userId is set", () => {
|
||||
const client = new MatrixClient({ baseUrl: "http://localhost" });
|
||||
expect(client.getUserIdLocalpart()).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the localpart of the userId", () => {
|
||||
expect(client.getUserIdLocalpart()).toBe("alice");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
|
||||
|
||||
@@ -112,7 +112,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||
await client!.initCrypto();
|
||||
await client!.initLegacyCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
@@ -227,7 +227,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => {
|
||||
await client!.initCrypto();
|
||||
await client!.initLegacyCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
@@ -556,7 +556,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should resolve incoming invites from /sync", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
syncData.rooms.join[roomOne].state!.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Invite,
|
||||
@@ -589,7 +589,7 @@ describe("MatrixClient syncing", () => {
|
||||
name: "The Ghost",
|
||||
}) as IMinimalEvent,
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
syncData.rooms.join[roomOne].state!.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Invite,
|
||||
@@ -617,7 +617,7 @@ describe("MatrixClient syncing", () => {
|
||||
name: "The Ghost",
|
||||
}) as IMinimalEvent,
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
syncData.rooms.join[roomOne].state!.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Invite,
|
||||
@@ -644,7 +644,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should no-op if resolveInvitesToProfiles is not set", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
syncData.rooms.join[roomOne].state!.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Invite,
|
||||
@@ -1373,6 +1373,114 @@ describe("MatrixClient syncing", () => {
|
||||
expect(stateEventEmitCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("msc4222", () => {
|
||||
const roomOneSyncOne = {
|
||||
"timeline": {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne,
|
||||
user: otherUserId,
|
||||
msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name",
|
||||
room: roomOne,
|
||||
user: otherUserId,
|
||||
content: {
|
||||
name: "Initial room name",
|
||||
},
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
const roomOneSyncTwo = {
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.topic",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: { topic: "A new room topic" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
"state": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: { name: "A new room name" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it("should ignore state events in timeline when state_after is present", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
join: { [roomOne]: roomOneSyncOne },
|
||||
},
|
||||
});
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
join: { [roomOne]: roomOneSyncTwo },
|
||||
},
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.name).toEqual("Initial room name");
|
||||
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
|
||||
"A new room topic",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should respect state events in state_after for left rooms", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
join: { [roomOne]: roomOneSyncOne },
|
||||
},
|
||||
});
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
leave: { [roomOne]: roomOneSyncTwo },
|
||||
},
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.name).toEqual("Initial room name");
|
||||
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
|
||||
"A new room topic",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeline", () => {
|
||||
@@ -2274,6 +2382,57 @@ describe("MatrixClient syncing", () => {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("msc4222", () => {
|
||||
it("should respect state events in state_after for left rooms", async () => {
|
||||
httpBackend!.when("POST", "/filter").respond(200, {
|
||||
filter_id: "another_id",
|
||||
});
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
leave: {
|
||||
[roomOne]: {
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.topic",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: { topic: "A new room topic" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
"state": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: { name: "A new room name" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [[room]] = await Promise.all([
|
||||
client!.syncLeftRooms(),
|
||||
|
||||
// first flush the filter request; this will make syncLeftRooms make its /sync call
|
||||
httpBackend!.flush("/filter").then(() => {
|
||||
return httpBackend!.flushAllExpected();
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(room.name).toEqual("Empty room");
|
||||
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
|
||||
"A new room topic",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("peek", () => {
|
||||
@@ -2414,7 +2573,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
|
||||
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
await idbClient.initCrypto();
|
||||
await idbClient.initLegacyCrypto();
|
||||
|
||||
const roomId = "!invite:example.org";
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
|
||||
const threadReply = thread.events.at(-1)!;
|
||||
await room.addLiveEvents([thread.rootEvent]);
|
||||
await room.addLiveEvents([thread.rootEvent], { addToState: false });
|
||||
|
||||
// Initialize read receipt datastructure before testing the reaction
|
||||
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
|
||||
|
||||
@@ -119,7 +119,7 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client!.initCrypto();
|
||||
await client!.initLegacyCrypto();
|
||||
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
|
||||
}
|
||||
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
|
||||
@@ -601,13 +601,13 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
initial: true,
|
||||
name: "Room with Invite",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee),
|
||||
],
|
||||
timeline: [],
|
||||
});
|
||||
await httpBackend!.flush("/profile", 1, 1000);
|
||||
await emitPromise(client!, RoomMemberEvent.Name);
|
||||
@@ -921,13 +921,12 @@ describe("SlidingSyncSdk", () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
|
||||
initial: true,
|
||||
});
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
@@ -962,13 +961,12 @@ describe("SlidingSyncSdk", () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
@@ -86,7 +86,7 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
|
||||
};
|
||||
|
||||
for (let i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
roomResponse.state!.events.push(
|
||||
mkMembershipCustom({
|
||||
membership: KnownMembership.Join,
|
||||
sender: roomMembers[i],
|
||||
@@ -561,7 +561,7 @@ export type InitCrypto = (_: MatrixClient) => Promise<void>;
|
||||
|
||||
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
|
||||
if (globalThis.Olm) {
|
||||
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
|
||||
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initLegacyCrypto();
|
||||
}
|
||||
|
||||
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
|
||||
|
||||
@@ -178,6 +178,6 @@ export const populateThread = ({
|
||||
}: MakeThreadProps): MakeThreadResult => {
|
||||
const ret = mkThread({ room, client, authorId, participantUserIds, length, ts });
|
||||
ret.thread.initialEventsFetched = true;
|
||||
room.addLiveEvents(ret.events);
|
||||
room.addLiveEvents(ret.events, { addToState: false });
|
||||
return ret;
|
||||
};
|
||||
|
||||
@@ -481,6 +481,9 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
public getUserId(): string {
|
||||
return this.userId;
|
||||
}
|
||||
public getSafeUserId(): string {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public getDeviceId(): string {
|
||||
return this.deviceId;
|
||||
|
||||
@@ -15,34 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { TextEncoder, TextDecoder } from "util";
|
||||
import NodeBuffer from "node:buffer";
|
||||
|
||||
import { decodeBase64, encodeBase64, encodeUnpaddedBase64, encodeUnpaddedBase64Url } from "../../src/base64";
|
||||
|
||||
describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
|
||||
let origBuffer = Buffer;
|
||||
|
||||
beforeAll(() => {
|
||||
if (env === "browser") {
|
||||
origBuffer = Buffer;
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-global-assign
|
||||
Buffer = undefined;
|
||||
|
||||
globalThis.atob = NodeBuffer.atob;
|
||||
globalThis.btoa = NodeBuffer.btoa;
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
Buffer = origBuffer;
|
||||
// @ts-ignore
|
||||
globalThis.atob = undefined;
|
||||
// @ts-ignore
|
||||
globalThis.btoa = undefined;
|
||||
});
|
||||
|
||||
describe("Base64 encoding", () => {
|
||||
it("Should decode properly encoded data", () => {
|
||||
const decoded = new TextDecoder().decode(decodeBase64("ZW5jb2RpbmcgaGVsbG8gd29ybGQ="));
|
||||
|
||||
|
||||
+16
-16
@@ -119,7 +119,7 @@ describe("Crypto", function () {
|
||||
|
||||
it("getVersion() should return the current version of the olm library", async () => {
|
||||
const client = new TestClient("@alice:example.com", "deviceid").client;
|
||||
await client.initCrypto();
|
||||
await client.initLegacyCrypto();
|
||||
|
||||
const olmVersionTuple = Crypto.getOlmVersion();
|
||||
expect(client.getCrypto()?.getVersion()).toBe(
|
||||
@@ -130,7 +130,7 @@ describe("Crypto", function () {
|
||||
describe("encrypted events", function () {
|
||||
it("provides encryption information for events from unverified senders", async function () {
|
||||
const client = new TestClient("@alice:example.com", "deviceid").client;
|
||||
await client.initCrypto();
|
||||
await client.initLegacyCrypto();
|
||||
|
||||
// unencrypted event
|
||||
const event = {
|
||||
@@ -210,7 +210,7 @@ describe("Crypto", function () {
|
||||
let client: MatrixClient;
|
||||
beforeEach(async () => {
|
||||
client = new TestClient("@alice:example.com", "deviceid").client;
|
||||
await client.initCrypto();
|
||||
await client.initLegacyCrypto();
|
||||
|
||||
// mock out the verification check
|
||||
client.crypto!.checkUserTrust = (userId) => new UserTrustLevel(true, false, false);
|
||||
@@ -306,7 +306,7 @@ describe("Crypto", function () {
|
||||
|
||||
it("doesn't throw an error when attempting to decrypt a redacted event", async () => {
|
||||
const client = new TestClient("@alice:example.com", "deviceid").client;
|
||||
await client.initCrypto();
|
||||
await client.initLegacyCrypto();
|
||||
|
||||
const event = new MatrixEvent({
|
||||
content: {},
|
||||
@@ -439,10 +439,10 @@ describe("Crypto", function () {
|
||||
secondAliceClient = new TestClient("@alice:example.com", "secondAliceDevice").client;
|
||||
bobClient = new TestClient("@bob:example.com", "bobdevice").client;
|
||||
claraClient = new TestClient("@clara:example.com", "claradevice").client;
|
||||
await aliceClient.initCrypto();
|
||||
await secondAliceClient.initCrypto();
|
||||
await bobClient.initCrypto();
|
||||
await claraClient.initCrypto();
|
||||
await aliceClient.initLegacyCrypto();
|
||||
await secondAliceClient.initLegacyCrypto();
|
||||
await bobClient.initLegacyCrypto();
|
||||
await claraClient.initLegacyCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
@@ -1111,7 +1111,7 @@ describe("Crypto", function () {
|
||||
jest.spyOn(logger, "debug").mockImplementation(() => {});
|
||||
jest.setTimeout(10000);
|
||||
const client = new TestClient("@a:example.com", "dev").client;
|
||||
await client.initCrypto();
|
||||
await client.initLegacyCrypto();
|
||||
client.crypto!.isCrossSigningReady = async () => false;
|
||||
client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
||||
client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
|
||||
@@ -1147,9 +1147,9 @@ describe("Crypto", function () {
|
||||
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
|
||||
// running initCrypto should trigger a key upload
|
||||
// running initLegacyCrypto 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)]);
|
||||
await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
|
||||
|
||||
encryptedPayload = {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
@@ -1264,9 +1264,9 @@ describe("Crypto", function () {
|
||||
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
|
||||
// running initCrypto should trigger a key upload
|
||||
// running initLegacyCrypto 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)]);
|
||||
await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
|
||||
|
||||
encryptedPayload = {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
@@ -1362,7 +1362,7 @@ describe("Crypto", function () {
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
await client.client.initCrypto();
|
||||
await client.client.initLegacyCrypto();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -1388,7 +1388,7 @@ describe("Crypto", function () {
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
await client.client.initCrypto();
|
||||
await client.client.initLegacyCrypto();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -1414,7 +1414,7 @@ describe("Crypto", function () {
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
await client.client.initCrypto();
|
||||
await client.client.initLegacyCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
|
||||
@@ -610,7 +610,11 @@ describe("MegolmDecryption", function () {
|
||||
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client;
|
||||
const bobClient2 = new TestClient("@bob:example.com", "bobdevice2").client;
|
||||
await Promise.all([aliceClient.initCrypto(), bobClient1.initCrypto(), bobClient2.initCrypto()]);
|
||||
await Promise.all([
|
||||
aliceClient.initLegacyCrypto(),
|
||||
bobClient1.initLegacyCrypto(),
|
||||
bobClient2.initLegacyCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient.crypto!.olmDevice;
|
||||
const bobDevice1 = bobClient1.crypto!.olmDevice;
|
||||
const bobDevice2 = bobClient2.crypto!.olmDevice;
|
||||
@@ -704,7 +708,7 @@ describe("MegolmDecryption", function () {
|
||||
it("does not block unverified devices when sending verification events", async function () {
|
||||
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
|
||||
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
|
||||
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
@@ -789,7 +793,7 @@ describe("MegolmDecryption", function () {
|
||||
it("notifies devices when unable to create olm session", async function () {
|
||||
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
|
||||
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
|
||||
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
|
||||
const aliceDevice = aliceClient.crypto!.olmDevice;
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
|
||||
@@ -873,7 +877,7 @@ describe("MegolmDecryption", function () {
|
||||
it("throws an error describing why it doesn't have a key", async function () {
|
||||
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
|
||||
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
|
||||
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
|
||||
const bobDevice = bobClient.crypto!.olmDevice;
|
||||
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
@@ -955,7 +959,7 @@ describe("MegolmDecryption", function () {
|
||||
it("throws an error describing the lack of an olm session", async function () {
|
||||
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
|
||||
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
|
||||
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
|
||||
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
|
||||
@@ -1051,7 +1055,7 @@ describe("MegolmDecryption", function () {
|
||||
it("throws an error to indicate a wedged olm session", async function () {
|
||||
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
|
||||
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
|
||||
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
|
||||
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
|
||||
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ describe("MegolmBackup", function () {
|
||||
|
||||
test("fail if given backup has no version", async () => {
|
||||
const client = makeTestClient(cryptoStore);
|
||||
await client.initCrypto();
|
||||
await client.initLegacyCrypto();
|
||||
const data = {
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
auth_data: {
|
||||
@@ -314,7 +314,7 @@ describe("MegolmBackup", function () {
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client
|
||||
.initCrypto()
|
||||
.initLegacyCrypto()
|
||||
.then(() => {
|
||||
return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
@@ -391,7 +391,7 @@ describe("MegolmBackup", function () {
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client
|
||||
.initCrypto()
|
||||
.initLegacyCrypto()
|
||||
.then(() => {
|
||||
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
|
||||
})
|
||||
@@ -471,7 +471,7 @@ describe("MegolmBackup", function () {
|
||||
// @ts-ignore private field access
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
await client.initCrypto();
|
||||
await client.initLegacyCrypto();
|
||||
client.uploadDeviceSigningKeys = async function (e) {
|
||||
return {};
|
||||
};
|
||||
@@ -560,7 +560,7 @@ describe("MegolmBackup", function () {
|
||||
// @ts-ignore private field access
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
await client.initCrypto();
|
||||
await client.initLegacyCrypto();
|
||||
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
@@ -636,7 +636,7 @@ describe("MegolmBackup", function () {
|
||||
// @ts-ignore private field access
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto();
|
||||
return client.initLegacyCrypto();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@@ -773,11 +773,19 @@ describe("MegolmBackup", function () {
|
||||
// initialising the crypto library will trigger a key upload request, which we can stub out
|
||||
client.uploadKeysRequest = jest.fn();
|
||||
|
||||
await client.initCrypto();
|
||||
await client.initLegacyCrypto();
|
||||
|
||||
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
|
||||
await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6);
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getKeyBackupInfo", () => {
|
||||
it("should return throw an `Not implemented`", async () => {
|
||||
const client = makeTestClient(cryptoStore);
|
||||
await client.initLegacyCrypto();
|
||||
await expect(client.getCrypto()?.getKeyBackupInfo()).rejects.toThrow("Not implemented");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ async function makeTestClient(
|
||||
const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options);
|
||||
const client = testClient.client;
|
||||
|
||||
await client.initCrypto();
|
||||
await client.initLegacyCrypto();
|
||||
|
||||
return { client, httpBackend: testClient.httpBackend };
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ describe("Dehydration", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await alice.client.initCrypto();
|
||||
await alice.client.initLegacyCrypto();
|
||||
|
||||
alice.httpBackend.when("GET", "/room_keys/version").respond(404, {
|
||||
errcode: "M_NOT_FOUND",
|
||||
|
||||
@@ -43,7 +43,7 @@ async function makeTestClient(
|
||||
return true;
|
||||
};
|
||||
|
||||
await client.initCrypto();
|
||||
await client.initLegacyCrypto();
|
||||
|
||||
// No need to download keys for these tests
|
||||
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue(new Map());
|
||||
|
||||
@@ -119,7 +119,7 @@ export async function makeTestClients(
|
||||
clients.push(testClient);
|
||||
}
|
||||
|
||||
await Promise.all(clients.map((testClient) => testClient.client.initCrypto()));
|
||||
await Promise.all(clients.map((testClient) => testClient.client.initLegacyCrypto()));
|
||||
|
||||
const destroy = () => {
|
||||
timeouts.forEach((t) => clearTimeout(t));
|
||||
|
||||
+146
-1
@@ -30,6 +30,7 @@ import {
|
||||
ITurnServer,
|
||||
IRoomEvent,
|
||||
IOpenIDCredentials,
|
||||
ISendEventFromWidgetResponseData,
|
||||
WidgetApiResponseError,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
@@ -40,6 +41,7 @@ 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",
|
||||
@@ -127,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();
|
||||
@@ -192,6 +201,142 @@ 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[3][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[3][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[3][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[3][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");
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("eventMapperFor", function () {
|
||||
const event = mapper(eventDefinition);
|
||||
expect(event).toBeInstanceOf(MatrixEvent);
|
||||
|
||||
room.addLiveEvents([event]);
|
||||
room.addLiveEvents([event], { addToState: false });
|
||||
expect(room.findEventById(eventId)).toBe(event);
|
||||
|
||||
const event2 = mapper(eventDefinition);
|
||||
@@ -109,7 +109,7 @@ describe("eventMapperFor", function () {
|
||||
|
||||
room.oldState.setStateEvents([event]);
|
||||
room.currentState.setStateEvents([event]);
|
||||
room.addLiveEvents([event]);
|
||||
room.addLiveEvents([event], { addToState: false });
|
||||
expect(room.findEventById(eventId)).toBe(event);
|
||||
|
||||
const event2 = mapper(eventDefinition);
|
||||
|
||||
@@ -104,7 +104,7 @@ describe("EventTimelineSet", () => {
|
||||
it("Adds event to the live timeline in the timeline set", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
eventTimelineSet.addLiveEvent(messageEvent);
|
||||
eventTimelineSet.addLiveEvent(messageEvent, { addToState: false });
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(1);
|
||||
});
|
||||
|
||||
@@ -113,6 +113,7 @@ describe("EventTimelineSet", () => {
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
eventTimelineSet.addLiveEvent(messageEvent, {
|
||||
duplicateStrategy: DuplicateStrategy.Replace,
|
||||
addToState: false,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(1);
|
||||
|
||||
@@ -130,6 +131,7 @@ describe("EventTimelineSet", () => {
|
||||
// replace.
|
||||
eventTimelineSet.addLiveEvent(duplicateMessageEvent, {
|
||||
duplicateStrategy: DuplicateStrategy.Replace,
|
||||
addToState: false,
|
||||
});
|
||||
|
||||
const eventsInLiveTimeline = liveTimeline.getEvents();
|
||||
@@ -144,6 +146,7 @@ describe("EventTimelineSet", () => {
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
addToState: false,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(1);
|
||||
});
|
||||
@@ -151,10 +154,17 @@ describe("EventTimelineSet", () => {
|
||||
it("Make sure legacy overload passing options directly as parameters still works", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(() => {
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true);
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
addToState: false,
|
||||
});
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true, false);
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
fromCache: false,
|
||||
addToState: false,
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -167,11 +177,13 @@ describe("EventTimelineSet", () => {
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
eventTimelineSet.addEventToTimeline(reactionEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
addToState: false,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
addToState: false,
|
||||
});
|
||||
expect(liveTimeline.getEvents()).toHaveLength(1);
|
||||
const [event] = liveTimeline.getEvents();
|
||||
@@ -202,6 +214,7 @@ describe("EventTimelineSet", () => {
|
||||
expect(() => {
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
|
||||
toStartOfTimeline: true,
|
||||
addToState: false,
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
@@ -214,6 +227,7 @@ describe("EventTimelineSet", () => {
|
||||
|
||||
eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
addToState: false,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
});
|
||||
@@ -232,6 +246,7 @@ describe("EventTimelineSet", () => {
|
||||
|
||||
eventTimelineSetForThread.addEventToTimeline(normalMessage, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
addToState: false,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
});
|
||||
@@ -248,6 +263,7 @@ describe("EventTimelineSet", () => {
|
||||
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(0);
|
||||
nonRoomEventTimelineSet.addEventToTimeline(messageEvent, nonRoomEventTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
addToState: false,
|
||||
});
|
||||
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(1);
|
||||
});
|
||||
@@ -257,7 +273,7 @@ describe("EventTimelineSet", () => {
|
||||
describe("aggregateRelations", () => {
|
||||
describe("with unencrypted events", () => {
|
||||
beforeEach(() => {
|
||||
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
|
||||
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
|
||||
});
|
||||
|
||||
itShouldReturnTheRelatedEvents();
|
||||
@@ -279,7 +295,7 @@ describe("EventTimelineSet", () => {
|
||||
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
|
||||
|
||||
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
|
||||
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
|
||||
});
|
||||
|
||||
it("should not return the related events", () => {
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("EventTimeline", function () {
|
||||
expect(function () {
|
||||
timeline.initialiseState(state);
|
||||
}).not.toThrow();
|
||||
timeline.addEvent(event, { toStartOfTimeline: false });
|
||||
timeline.addEvent(event, { toStartOfTimeline: false, addToState: false });
|
||||
expect(function () {
|
||||
timeline.initialiseState(state);
|
||||
}).toThrow();
|
||||
@@ -182,9 +182,9 @@ describe("EventTimeline", function () {
|
||||
];
|
||||
|
||||
it("should be able to add events to the end", function () {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getEvents()[0]).toEqual(events[0]);
|
||||
@@ -192,9 +192,9 @@ describe("EventTimeline", function () {
|
||||
});
|
||||
|
||||
it("should be able to add events to the start", function () {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false });
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: false });
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getEvents()[0]).toEqual(events[1]);
|
||||
@@ -238,9 +238,9 @@ describe("EventTimeline", function () {
|
||||
content: { name: "Old Room Name" },
|
||||
});
|
||||
|
||||
timeline.addEvent(newEv, { toStartOfTimeline: false });
|
||||
timeline.addEvent(newEv, { toStartOfTimeline: false, addToState: false });
|
||||
expect(newEv.sender).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, { toStartOfTimeline: true });
|
||||
timeline.addEvent(oldEv, { toStartOfTimeline: true, addToState: false });
|
||||
expect(oldEv.sender).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
@@ -280,9 +280,9 @@ describe("EventTimeline", function () {
|
||||
skey: userA,
|
||||
event: true,
|
||||
});
|
||||
timeline.addEvent(newEv, { toStartOfTimeline: false });
|
||||
timeline.addEvent(newEv, { toStartOfTimeline: false, addToState: false });
|
||||
expect(newEv.target).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, { toStartOfTimeline: true });
|
||||
timeline.addEvent(oldEv, { toStartOfTimeline: true, addToState: false });
|
||||
expect(oldEv.target).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
@@ -308,8 +308,8 @@ describe("EventTimeline", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: true });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: true });
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
|
||||
timelineWasEmpty: undefined,
|
||||
@@ -347,8 +347,8 @@ describe("EventTimeline", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: true });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: true });
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
|
||||
timelineWasEmpty: undefined,
|
||||
@@ -365,11 +365,15 @@ describe("EventTimeline", function () {
|
||||
);
|
||||
|
||||
it("Make sure legacy overload passing options directly as parameters still works", () => {
|
||||
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow();
|
||||
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false })).not.toThrow();
|
||||
// @ts-ignore stateContext is not a valid param
|
||||
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow();
|
||||
expect(() =>
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false, roomState: new RoomState(roomId) }),
|
||||
timeline.addEvent(events[0], {
|
||||
toStartOfTimeline: false,
|
||||
addToState: false,
|
||||
roomState: new RoomState(roomId),
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -397,8 +401,8 @@ describe("EventTimeline", function () {
|
||||
];
|
||||
|
||||
it("should remove events", function () {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
|
||||
let ev = timeline.removeEvent(events[0].getId()!);
|
||||
@@ -411,9 +415,9 @@ describe("EventTimeline", function () {
|
||||
});
|
||||
|
||||
it("should update baseIndex", function () {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[2], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: false });
|
||||
timeline.addEvent(events[2], { toStartOfTimeline: false, addToState: false });
|
||||
expect(timeline.getEvents().length).toEqual(3);
|
||||
expect(timeline.getBaseIndex()).toEqual(1);
|
||||
|
||||
@@ -430,11 +434,11 @@ describe("EventTimeline", function () {
|
||||
// - removing the last event got baseIndex into such a state that
|
||||
// further addEvent(ev, false) calls made the index increase.
|
||||
it("should not make baseIndex assplode when removing the last event", function () {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false });
|
||||
timeline.removeEvent(events[0].getId()!);
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[2], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
|
||||
timeline.addEvent(events[2], { toStartOfTimeline: false, addToState: false });
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
});
|
||||
|
||||
@@ -2799,24 +2799,28 @@ describe("MatrixClient", function () {
|
||||
roomCreateEvent(room1.roomId, replacedByCreate1.roomId),
|
||||
predecessorEvent(room1.roomId, replacedByDynamicPredecessor1.roomId),
|
||||
],
|
||||
{},
|
||||
{ addToState: true },
|
||||
);
|
||||
room2.addLiveEvents(
|
||||
[
|
||||
roomCreateEvent(room2.roomId, replacedByCreate2.roomId),
|
||||
predecessorEvent(room2.roomId, replacedByDynamicPredecessor2.roomId),
|
||||
],
|
||||
{},
|
||||
{ addToState: true },
|
||||
);
|
||||
replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {});
|
||||
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {});
|
||||
replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {
|
||||
addToState: true,
|
||||
});
|
||||
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {
|
||||
addToState: true,
|
||||
});
|
||||
replacedByDynamicPredecessor1.addLiveEvents(
|
||||
[tombstoneEvent(room1.roomId, replacedByDynamicPredecessor1.roomId)],
|
||||
{},
|
||||
{ addToState: true },
|
||||
);
|
||||
replacedByDynamicPredecessor2.addLiveEvents(
|
||||
[tombstoneEvent(room2.roomId, replacedByDynamicPredecessor2.roomId)],
|
||||
{},
|
||||
{ addToState: true },
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -2854,10 +2858,10 @@ describe("MatrixClient", function () {
|
||||
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2];
|
||||
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], {});
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], {});
|
||||
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], {});
|
||||
replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], {});
|
||||
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
|
||||
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
|
||||
replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
|
||||
|
||||
// When we ask for the visible rooms
|
||||
const rooms = client.getVisibleRooms();
|
||||
@@ -2937,15 +2941,15 @@ describe("MatrixClient", function () {
|
||||
const room4 = new Room("room4", client, "@michonne:hawthorne.example.com");
|
||||
|
||||
if (creates) {
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)]);
|
||||
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)]);
|
||||
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)]);
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)], { addToState: true });
|
||||
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)], { addToState: true });
|
||||
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)], { addToState: true });
|
||||
}
|
||||
|
||||
if (tombstones) {
|
||||
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], {});
|
||||
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], {});
|
||||
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], {});
|
||||
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], { addToState: true });
|
||||
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], { addToState: true });
|
||||
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], { addToState: true });
|
||||
}
|
||||
|
||||
mocked(store.getRoom).mockImplementation((roomId: string) => {
|
||||
@@ -2980,17 +2984,17 @@ describe("MatrixClient", function () {
|
||||
const dynRoom4 = new Room("dynRoom4", client, "@rick:grimes.example.com");
|
||||
const dynRoom5 = new Room("dynRoom5", client, "@rick:grimes.example.com");
|
||||
|
||||
dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], {});
|
||||
dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)]);
|
||||
dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], { addToState: true });
|
||||
dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)], { addToState: true });
|
||||
|
||||
dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], {});
|
||||
room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)]);
|
||||
dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], { addToState: true });
|
||||
room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)], { addToState: true });
|
||||
|
||||
room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], {});
|
||||
dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)]);
|
||||
room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], { addToState: true });
|
||||
dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)], { addToState: true });
|
||||
|
||||
dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], {});
|
||||
dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)]);
|
||||
dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], { addToState: true });
|
||||
dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)], { addToState: true });
|
||||
|
||||
mocked(store.getRoom)
|
||||
.mockClear()
|
||||
|
||||
@@ -38,6 +38,8 @@ const membershipTemplate: CallMembershipData = {
|
||||
|
||||
const mockFocus = { type: "mock" };
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
describe("MatrixRTCSession", () => {
|
||||
let client: MatrixClient;
|
||||
let sess: MatrixRTCSession | undefined;
|
||||
@@ -864,7 +866,64 @@ describe("MatrixRTCSession", () => {
|
||||
expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel);
|
||||
});
|
||||
|
||||
it("Re-sends key if a new member joins", async () => {
|
||||
it("re-sends key if a new member joins even if a key rotation is in progress", async () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
// session with two members
|
||||
const member2 = Object.assign({}, membershipTemplate, {
|
||||
device_id: "BBBBBBB",
|
||||
});
|
||||
const mockRoom = makeMockRoom([membershipTemplate, member2]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
|
||||
// joining will trigger an initial key send
|
||||
const keysSentPromise1 = new Promise<EncryptionKeysEventContent>((resolve) => {
|
||||
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
|
||||
});
|
||||
sess.joinRoomSession([mockFocus], mockFocus, {
|
||||
manageMediaKeys: true,
|
||||
updateEncryptionKeyThrottle: 1000,
|
||||
makeKeyDelay: 3000,
|
||||
});
|
||||
await keysSentPromise1;
|
||||
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
|
||||
|
||||
// member2 leaves triggering key rotation
|
||||
mockRoom.getLiveTimeline().getState = jest
|
||||
.fn()
|
||||
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
|
||||
sess.onMembershipUpdate();
|
||||
|
||||
// member2 re-joins which should trigger an immediate re-send
|
||||
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
|
||||
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
|
||||
});
|
||||
mockRoom.getLiveTimeline().getState = jest
|
||||
.fn()
|
||||
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
|
||||
sess.onMembershipUpdate();
|
||||
// but, that immediate resend is throttled so we need to wait a bit
|
||||
jest.advanceTimersByTime(1000);
|
||||
const { keys } = await keysSentPromise2;
|
||||
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
|
||||
// key index should still be the original: 0
|
||||
expect(keys[0].index).toEqual(0);
|
||||
|
||||
// check that the key rotation actually happens
|
||||
const keysSentPromise3 = new Promise<EncryptionKeysEventContent>((resolve) => {
|
||||
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
|
||||
});
|
||||
jest.advanceTimersByTime(2000);
|
||||
const { keys: rotatedKeys } = await keysSentPromise3;
|
||||
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(3);
|
||||
// key index should now be the rotated one: 1
|
||||
expect(rotatedKeys[0].index).toEqual(1);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("re-sends key if a new member joins", async () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
@@ -1288,7 +1347,7 @@ describe("MatrixRTCSession", () => {
|
||||
sess!.reemitEncryptionKeys();
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
|
||||
Buffer.from("this is the key", "utf-8"),
|
||||
textEncoder.encode("this is the key"),
|
||||
0,
|
||||
"@bob:example.org:bobsphone",
|
||||
);
|
||||
@@ -1320,7 +1379,7 @@ describe("MatrixRTCSession", () => {
|
||||
sess!.reemitEncryptionKeys();
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
|
||||
Buffer.from("this is the key", "utf-8"),
|
||||
textEncoder.encode("this is the key"),
|
||||
4,
|
||||
"@bob:example.org:bobsphone",
|
||||
);
|
||||
@@ -1352,7 +1411,7 @@ describe("MatrixRTCSession", () => {
|
||||
sess!.reemitEncryptionKeys();
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
|
||||
Buffer.from("this is the key", "utf-8"),
|
||||
textEncoder.encode("this is the key"),
|
||||
0,
|
||||
"@bob:example.org:bobsphone",
|
||||
);
|
||||
@@ -1379,12 +1438,12 @@ describe("MatrixRTCSession", () => {
|
||||
sess!.reemitEncryptionKeys();
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(2);
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
|
||||
Buffer.from("this is the key", "utf-8"),
|
||||
textEncoder.encode("this is the key"),
|
||||
0,
|
||||
"@bob:example.org:bobsphone",
|
||||
);
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
|
||||
Buffer.from("this is the key", "utf-8"),
|
||||
textEncoder.encode("this is the key"),
|
||||
4,
|
||||
"@bob:example.org:bobsphone",
|
||||
);
|
||||
@@ -1432,7 +1491,7 @@ describe("MatrixRTCSession", () => {
|
||||
sess!.reemitEncryptionKeys();
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
|
||||
Buffer.from("newer key", "utf-8"),
|
||||
textEncoder.encode("newer key"),
|
||||
0,
|
||||
"@bob:example.org:bobsphone",
|
||||
);
|
||||
@@ -1480,7 +1539,7 @@ describe("MatrixRTCSession", () => {
|
||||
sess!.reemitEncryptionKeys();
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
|
||||
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
|
||||
Buffer.from("second key", "utf-8"),
|
||||
textEncoder.encode("second key"),
|
||||
0,
|
||||
"@bob:example.org:bobsphone",
|
||||
);
|
||||
|
||||
@@ -99,7 +99,7 @@ describe("MatrixEvent", () => {
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const ev = createEvent("$event1:server");
|
||||
|
||||
await room.addLiveEvents([ev]);
|
||||
await room.addLiveEvents([ev], { addToState: false });
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(ev.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]);
|
||||
@@ -120,7 +120,7 @@ describe("MatrixEvent", () => {
|
||||
const threadRoot = createEvent("$threadroot:server");
|
||||
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
|
||||
|
||||
await room.addLiveEvents([threadRoot, ev]);
|
||||
await room.addLiveEvents([threadRoot, ev], { addToState: false });
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(threadRoot.threadRootId).toEqual(threadRoot.getId());
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
|
||||
@@ -143,7 +143,7 @@ describe("MatrixEvent", () => {
|
||||
const threadRoot = createEvent("$threadroot:server");
|
||||
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
|
||||
|
||||
await room.addLiveEvents([threadRoot, ev]);
|
||||
await room.addLiveEvents([threadRoot, ev], { addToState: false });
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(ev.threadRootId).toEqual(threadRoot.getId());
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
|
||||
@@ -167,7 +167,7 @@ describe("MatrixEvent", () => {
|
||||
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
|
||||
const reaction = createReactionEvent("$reaction:server", ev.getId()!);
|
||||
|
||||
await room.addLiveEvents([threadRoot, ev, reaction]);
|
||||
await room.addLiveEvents([threadRoot, ev, reaction], { addToState: false });
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(reaction.threadRootId).toEqual(threadRoot.getId());
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
|
||||
@@ -191,7 +191,7 @@ describe("MatrixEvent", () => {
|
||||
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
|
||||
const edit = createEditEvent("$edit:server", ev.getId()!);
|
||||
|
||||
await room.addLiveEvents([threadRoot, ev, edit]);
|
||||
await room.addLiveEvents([threadRoot, ev, edit], { addToState: false });
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(edit.threadRootId).toEqual(threadRoot.getId());
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
|
||||
@@ -217,7 +217,7 @@ describe("MatrixEvent", () => {
|
||||
const reply2 = createReplyEvent("$reply2:server", reply1.getId()!);
|
||||
const reaction = createReactionEvent("$reaction:server", reply2.getId()!);
|
||||
|
||||
await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction]);
|
||||
await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction], { addToState: false });
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(reaction.threadRootId).toEqual(threadRoot.getId());
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("RoomReceipts", () => {
|
||||
// Given there are no receipts in the room
|
||||
const room = createRoom();
|
||||
const [event] = createEvent();
|
||||
room.addLiveEvents([event]);
|
||||
room.addLiveEvents([event], { addToState: false });
|
||||
|
||||
// When I ask about any event, then it is unread
|
||||
expect(room.hasUserReadEvent(readerId, event.getId()!)).toBe(false);
|
||||
@@ -46,7 +46,7 @@ describe("RoomReceipts", () => {
|
||||
// Given there are no receipts in the room
|
||||
const room = createRoom();
|
||||
const [event] = createEventSentBy(readerId);
|
||||
room.addLiveEvents([event]);
|
||||
room.addLiveEvents([event], { addToState: false });
|
||||
|
||||
// When I ask about an event I sent, it is read (because a synthetic
|
||||
// receipt was created and stored in RoomReceipts)
|
||||
@@ -57,7 +57,7 @@ describe("RoomReceipts", () => {
|
||||
// Given my event exists and is unread
|
||||
const room = createRoom();
|
||||
const [event, eventId] = createEvent();
|
||||
room.addLiveEvents([event]);
|
||||
room.addLiveEvents([event], { addToState: false });
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// When we receive a receipt for this event+user
|
||||
@@ -72,7 +72,7 @@ describe("RoomReceipts", () => {
|
||||
const room = createRoom();
|
||||
const [event1, event1Id] = createEvent();
|
||||
const [event2] = createEvent();
|
||||
room.addLiveEvents([event1, event2]);
|
||||
room.addLiveEvents([event1, event2], { addToState: false });
|
||||
|
||||
// When we receive a receipt for the later event
|
||||
room.addReceipt(createReceipt(readerId, event2));
|
||||
@@ -86,7 +86,7 @@ describe("RoomReceipts", () => {
|
||||
const room = createRoom();
|
||||
const [oldEvent, oldEventId] = createEvent();
|
||||
const [liveEvent] = createEvent();
|
||||
room.addLiveEvents([liveEvent]);
|
||||
room.addLiveEvents([liveEvent], { addToState: false });
|
||||
createOldTimeline(room, [oldEvent]);
|
||||
|
||||
// When we receive a receipt for the live event
|
||||
@@ -120,7 +120,7 @@ describe("RoomReceipts", () => {
|
||||
const room = createRoom();
|
||||
const [event1] = createEvent();
|
||||
const [event2, event2Id] = createEvent();
|
||||
room.addLiveEvents([event1, event2]);
|
||||
room.addLiveEvents([event1, event2], { addToState: false });
|
||||
|
||||
// When we receive a receipt for the earlier event
|
||||
room.addReceipt(createReceipt(readerId, event1));
|
||||
@@ -133,7 +133,7 @@ describe("RoomReceipts", () => {
|
||||
// Given my event exists and is unread
|
||||
const room = createRoom();
|
||||
const [event, eventId] = createEvent();
|
||||
room.addLiveEvents([event]);
|
||||
room.addLiveEvents([event], { addToState: false });
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// When we receive a receipt for another user
|
||||
@@ -151,7 +151,7 @@ describe("RoomReceipts", () => {
|
||||
const room = createRoom();
|
||||
const [previousEvent] = createEvent();
|
||||
const [myEvent] = createEventSentBy(readerId);
|
||||
room.addLiveEvents([previousEvent, myEvent]);
|
||||
room.addLiveEvents([previousEvent, myEvent], { addToState: false });
|
||||
|
||||
// And I just received a receipt for the previous event
|
||||
room.addReceipt(createReceipt(readerId, previousEvent));
|
||||
@@ -165,7 +165,7 @@ describe("RoomReceipts", () => {
|
||||
const room = createRoom();
|
||||
const [myEvent] = createEventSentBy(readerId);
|
||||
const [laterEvent] = createEvent();
|
||||
room.addLiveEvents([myEvent, laterEvent]);
|
||||
room.addLiveEvents([myEvent, laterEvent], { addToState: false });
|
||||
|
||||
// When I ask about the later event, it is unread (because it's after the synthetic receipt)
|
||||
expect(room.hasUserReadEvent(readerId, laterEvent.getId()!)).toBe(false);
|
||||
@@ -177,7 +177,7 @@ describe("RoomReceipts", () => {
|
||||
const [event1] = createEvent();
|
||||
const [event2, event2Id] = createEvent();
|
||||
const [event3, event3Id] = createEvent();
|
||||
room.addLiveEvents([event1, event2, event3]);
|
||||
room.addLiveEvents([event1, event2, event3], { addToState: false });
|
||||
|
||||
// When we receive receipts for the older events out of order
|
||||
room.addReceipt(createReceipt(readerId, event2));
|
||||
@@ -192,7 +192,7 @@ describe("RoomReceipts", () => {
|
||||
// Given my event exists and is unread
|
||||
const room = createRoom();
|
||||
const [event, eventId] = createEvent();
|
||||
room.addLiveEvents([event]);
|
||||
room.addLiveEvents([event], { addToState: false });
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// When we receive a receipt for this event+user
|
||||
@@ -208,7 +208,7 @@ describe("RoomReceipts", () => {
|
||||
const [root, rootId] = createEvent();
|
||||
const [event, eventId] = createThreadedEvent(root);
|
||||
setupThread(room, root);
|
||||
room.addLiveEvents([root, event]);
|
||||
room.addLiveEvents([root, event], { addToState: false });
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// When we receive a receipt for this event on this thread
|
||||
@@ -225,7 +225,7 @@ describe("RoomReceipts", () => {
|
||||
const [event1, event1Id] = createThreadedEvent(root);
|
||||
const [event2] = createThreadedEvent(root);
|
||||
setupThread(room, root);
|
||||
room.addLiveEvents([root, event1, event2]);
|
||||
room.addLiveEvents([root, event1, event2], { addToState: false });
|
||||
|
||||
// When we receive a receipt for the later event
|
||||
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
|
||||
@@ -241,7 +241,7 @@ describe("RoomReceipts", () => {
|
||||
const [event1] = createThreadedEvent(root);
|
||||
const [event2, event2Id] = createThreadedEvent(root);
|
||||
setupThread(room, root);
|
||||
room.addLiveEvents([root, event1, event2]);
|
||||
room.addLiveEvents([root, event1, event2], { addToState: false });
|
||||
|
||||
// When we receive a receipt for the earlier event
|
||||
room.addReceipt(createThreadedReceipt(readerId, event1, rootId));
|
||||
@@ -256,7 +256,7 @@ describe("RoomReceipts", () => {
|
||||
const [root, rootId] = createEvent();
|
||||
const [event, eventId] = createThreadedEvent(root);
|
||||
setupThread(room, root);
|
||||
room.addLiveEvents([root, event]);
|
||||
room.addLiveEvents([root, event], { addToState: false });
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
|
||||
|
||||
// When we receive a receipt for another user
|
||||
@@ -278,7 +278,7 @@ describe("RoomReceipts", () => {
|
||||
const [thread2] = createThreadedEvent(root2);
|
||||
setupThread(room, root1);
|
||||
setupThread(room, root2);
|
||||
room.addLiveEvents([root1, root2, thread1, thread2]);
|
||||
room.addLiveEvents([root1, root2, thread1, thread2], { addToState: false });
|
||||
|
||||
// When we receive a receipt for the later event
|
||||
room.addReceipt(createThreadedReceipt(readerId, thread2, root2.getId()!));
|
||||
@@ -295,7 +295,7 @@ describe("RoomReceipts", () => {
|
||||
const [event2, event2Id] = createThreadedEvent(root);
|
||||
const [event3, event3Id] = createThreadedEvent(root);
|
||||
setupThread(room, root);
|
||||
room.addLiveEvents([root, event1, event2, event3]);
|
||||
room.addLiveEvents([root, event1, event2, event3], { addToState: false });
|
||||
|
||||
// When we receive receipts for the older events out of order
|
||||
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
|
||||
@@ -329,7 +329,7 @@ describe("RoomReceipts", () => {
|
||||
const [thread2b, thread2bId] = createThreadedEvent(main2);
|
||||
setupThread(room, main1);
|
||||
setupThread(room, main2);
|
||||
room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b]);
|
||||
room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b], { addToState: false });
|
||||
|
||||
// And the timestamps on the events are consistent with the order above
|
||||
main1.event.origin_server_ts = 1;
|
||||
@@ -377,7 +377,7 @@ describe("RoomReceipts", () => {
|
||||
|
||||
// Add the event to the room
|
||||
// The receipt is removed from the dangling state
|
||||
room.addLiveEvents([event]);
|
||||
room.addLiveEvents([event], { addToState: false });
|
||||
|
||||
// Then the event is read
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
|
||||
@@ -398,7 +398,7 @@ describe("RoomReceipts", () => {
|
||||
|
||||
// Add the events to the room
|
||||
// The receipt is removed from the dangling state
|
||||
room.addLiveEvents([root, event]);
|
||||
room.addLiveEvents([root, event], { addToState: false });
|
||||
|
||||
// Then the event is read
|
||||
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
|
||||
@@ -418,7 +418,7 @@ describe("RoomReceipts", () => {
|
||||
|
||||
// Add the event to the room
|
||||
// The two receipts should be processed
|
||||
room.addLiveEvents([event]);
|
||||
room.addLiveEvents([event], { addToState: false });
|
||||
|
||||
// Then the event is read
|
||||
// We expect that the receipt of `otherUserId` didn't replace/erase the receipt of `readerId`
|
||||
@@ -528,7 +528,7 @@ function createThreadedReceipt(userId: string, referencedEvent: MatrixEvent, thr
|
||||
*/
|
||||
function createOldTimeline(room: Room, events: MatrixEvent[]) {
|
||||
const oldTimeline = room.getUnfilteredTimelineSet().addTimeline();
|
||||
room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, oldTimeline);
|
||||
room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, false, oldTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -801,7 +801,7 @@ async function createThread(client: MatrixClient, user: string, roomId: string):
|
||||
|
||||
// Ensure the root is in the room timeline
|
||||
root.setThreadId(root.getId());
|
||||
await room.addLiveEvents([root]);
|
||||
await room.addLiveEvents([root], { addToState: false });
|
||||
|
||||
// Create the thread and wait for it to be initialised
|
||||
const thread = room.createThread(root.getId()!, root, [], false);
|
||||
|
||||
@@ -106,7 +106,7 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
mockClient,
|
||||
);
|
||||
|
||||
room.addLiveEvents([event]);
|
||||
room.addLiveEvents([event], { addToState: false });
|
||||
|
||||
THREAD_ID = event.getId()!;
|
||||
threadEvent = mkEvent({
|
||||
|
||||
@@ -198,8 +198,8 @@ describe("Relations", function () {
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
timelineSet.addLiveEvent(targetEvent);
|
||||
timelineSet.addLiveEvent(relationEvent);
|
||||
timelineSet.addLiveEvent(targetEvent, { addToState: false });
|
||||
timelineSet.addLiveEvent(relationEvent, { addToState: false });
|
||||
|
||||
await relationsCreated;
|
||||
}
|
||||
@@ -212,8 +212,8 @@ describe("Relations", function () {
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
timelineSet.addLiveEvent(relationEvent);
|
||||
timelineSet.addLiveEvent(targetEvent);
|
||||
timelineSet.addLiveEvent(relationEvent, { addToState: false });
|
||||
timelineSet.addLiveEvent(targetEvent, { addToState: false });
|
||||
|
||||
await relationsCreated;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -1122,59 +1122,4 @@ describe("RoomState", function () {
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
describe("skipWrongOrderRoomStateInserts", () => {
|
||||
const idNow = "now";
|
||||
const idBefore = "before";
|
||||
const endState = new RoomState(roomId);
|
||||
const startState = new RoomState(roomId, undefined, true);
|
||||
|
||||
let onRoomStateEvent: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
|
||||
const evNow = new MatrixEvent({
|
||||
type: "m.call.member",
|
||||
room_id: roomId,
|
||||
state_key: "@user:example.org",
|
||||
content: {},
|
||||
});
|
||||
evNow.event.unsigned = { replaces_state: idBefore };
|
||||
evNow.event.event_id = idNow;
|
||||
|
||||
const evBefore = new MatrixEvent({
|
||||
type: "m.call.member",
|
||||
room_id: roomId,
|
||||
state_key: "@user:example.org",
|
||||
content: {},
|
||||
});
|
||||
|
||||
const updatedStateWithBefore = jest.fn();
|
||||
const updatedStateWithNow = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
evBefore.event.event_id = idBefore;
|
||||
onRoomStateEvent = (event, _state, _lastState) => {
|
||||
if (event.event.event_id === idNow) {
|
||||
updatedStateWithNow();
|
||||
} else if (event.event.event_id === idBefore) {
|
||||
updatedStateWithBefore();
|
||||
}
|
||||
};
|
||||
endState.on(RoomStateEvent.Events, onRoomStateEvent);
|
||||
startState.on(RoomStateEvent.Events, onRoomStateEvent);
|
||||
});
|
||||
afterEach(() => {
|
||||
endState.off(RoomStateEvent.Events, onRoomStateEvent);
|
||||
startState.off(RoomStateEvent.Events, onRoomStateEvent);
|
||||
updatedStateWithNow.mockReset();
|
||||
updatedStateWithBefore.mockReset();
|
||||
});
|
||||
it("should skip inserting state to the end of the timeline if the current endState events replaces_state id is the same as the inserted events id", () => {
|
||||
endState.setStateEvents([evNow, evBefore]);
|
||||
expect(updatedStateWithBefore).not.toHaveBeenCalled();
|
||||
expect(updatedStateWithNow).toHaveBeenCalled();
|
||||
});
|
||||
it("should skip inserting state at the beginning of the timeline if the inserted events replaces_state is the event id of the current startState", () => {
|
||||
startState.setStateEvents([evBefore, evNow]);
|
||||
expect(updatedStateWithBefore).toHaveBeenCalled();
|
||||
expect(updatedStateWithNow).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+594
-437
File diff suppressed because it is too large
Load Diff
@@ -572,15 +572,37 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
|
||||
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
|
||||
rustCrypto = await makeTestRustCrypto(
|
||||
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
|
||||
baseUrl: "http://server/",
|
||||
prefix: "",
|
||||
onlyData: true,
|
||||
}),
|
||||
testData.TEST_USER_ID,
|
||||
);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", {
|
||||
device_keys: {
|
||||
[testData.TEST_USER_ID]: {
|
||||
[testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// wait until we know about the other device
|
||||
rustCrypto.onSyncCompleted({});
|
||||
await rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]);
|
||||
|
||||
const toDeviceEvent = {
|
||||
type: "m.key.verification.request",
|
||||
content: {
|
||||
from_device: "testDeviceId",
|
||||
from_device: testData.TEST_DEVICE_ID,
|
||||
methods: ["m.sas.v1"],
|
||||
transaction_id: "testTxn",
|
||||
timestamp: Date.now() - 1000,
|
||||
},
|
||||
sender: "@user:id",
|
||||
sender: testData.TEST_USER_ID,
|
||||
};
|
||||
|
||||
const onEvent = jest.fn();
|
||||
@@ -991,23 +1013,41 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, null],
|
||||
["Encrypted by an unverified user.", EventShieldReason.UNVERIFIED_IDENTITY],
|
||||
["Encrypted by a device not verified by its owner.", EventShieldReason.UNSIGNED_DEVICE],
|
||||
[undefined, undefined, null],
|
||||
[
|
||||
"Encrypted by an unverified user.",
|
||||
RustSdkCryptoJs.ShieldStateCode.UnverifiedIdentity,
|
||||
EventShieldReason.UNVERIFIED_IDENTITY,
|
||||
],
|
||||
[
|
||||
"Encrypted by a device not verified by its owner.",
|
||||
RustSdkCryptoJs.ShieldStateCode.UnsignedDevice,
|
||||
EventShieldReason.UNSIGNED_DEVICE,
|
||||
],
|
||||
[
|
||||
"The authenticity of this encrypted message can't be guaranteed on this device.",
|
||||
RustSdkCryptoJs.ShieldStateCode.AuthenticityNotGuaranteed,
|
||||
EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
|
||||
],
|
||||
["Encrypted by an unknown or deleted device.", EventShieldReason.UNKNOWN_DEVICE],
|
||||
["bloop", EventShieldReason.UNKNOWN],
|
||||
])("gets the right shield reason (%s)", async (rustReason, expectedReason) => {
|
||||
[
|
||||
"Encrypted by an unknown or deleted device.",
|
||||
RustSdkCryptoJs.ShieldStateCode.UnknownDevice,
|
||||
EventShieldReason.UNKNOWN_DEVICE,
|
||||
],
|
||||
["Not encrypted.", RustSdkCryptoJs.ShieldStateCode.SentInClear, EventShieldReason.SENT_IN_CLEAR],
|
||||
[
|
||||
"Encrypted by a previously-verified user who is no longer verified.",
|
||||
RustSdkCryptoJs.ShieldStateCode.VerificationViolation,
|
||||
EventShieldReason.VERIFICATION_VIOLATION,
|
||||
],
|
||||
])("gets the right shield reason (%s)", async (rustReason, rustCode, expectedReason) => {
|
||||
// suppress the warning from the unknown shield reason
|
||||
jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
const mockEncryptionInfo = {
|
||||
shieldState: jest
|
||||
.fn()
|
||||
.mockReturnValue({ color: RustSdkCryptoJs.ShieldColor.None, message: rustReason }),
|
||||
.mockReturnValue({ color: RustSdkCryptoJs.ShieldColor.None, code: rustCode, message: rustReason }),
|
||||
} as unknown as RustSdkCryptoJs.EncryptionInfo;
|
||||
olmMachine.getRoomEventEncryptionInfo.mockResolvedValue(mockEncryptionInfo);
|
||||
|
||||
@@ -1526,6 +1566,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", () => {
|
||||
|
||||
@@ -17,8 +17,10 @@ limitations under the License.
|
||||
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import {
|
||||
IJoinedRoom,
|
||||
Category,
|
||||
IInvitedRoom,
|
||||
IInviteState,
|
||||
IJoinedRoom,
|
||||
IKnockedRoom,
|
||||
IKnockState,
|
||||
ILeftRoom,
|
||||
@@ -27,7 +29,6 @@ import {
|
||||
IStrippedState,
|
||||
ISyncResponse,
|
||||
SyncAccumulator,
|
||||
IInviteState,
|
||||
} from "../../src/sync-accumulator";
|
||||
import { IRoomSummary } from "../../src";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
@@ -85,6 +86,7 @@ describe("SyncAccumulator", function () {
|
||||
// technically cheating since we also cheekily pre-populate keys we
|
||||
// know that the sync accumulator will pre-populate.
|
||||
// It isn't 100% transitive.
|
||||
const events = [member("alice", KnownMembership.Join), member("bob", KnownMembership.Join)];
|
||||
const res = {
|
||||
next_batch: "abc",
|
||||
rooms: {
|
||||
@@ -92,18 +94,17 @@ describe("SyncAccumulator", function () {
|
||||
leave: {},
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
account_data: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
unread_notifications: {},
|
||||
state: {
|
||||
events: [member("alice", KnownMembership.Join), member("bob", KnownMembership.Join)],
|
||||
},
|
||||
summary: {
|
||||
"account_data": { events: [] },
|
||||
"ephemeral": { events: [] },
|
||||
"unread_notifications": {},
|
||||
"org.matrix.msc4222.state_after": { events },
|
||||
"state": { events },
|
||||
"summary": {
|
||||
"m.heroes": undefined,
|
||||
"m.joined_member_count": undefined,
|
||||
"m.invited_member_count": undefined,
|
||||
},
|
||||
timeline: {
|
||||
"timeline": {
|
||||
events: [msg("alice", "hi")],
|
||||
prev_batch: "something",
|
||||
},
|
||||
@@ -882,6 +883,147 @@ describe("SyncAccumulator", function () {
|
||||
).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("msc4222", () => {
|
||||
it("should accumulate state_after events", () => {
|
||||
const initState = {
|
||||
events: [member("alice", KnownMembership.Knock)],
|
||||
};
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
"org.matrix.msc4222.state_after": initState,
|
||||
}),
|
||||
);
|
||||
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
|
||||
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
user: "alice",
|
||||
room: "!knock:bar",
|
||||
type: "m.room.name",
|
||||
content: {
|
||||
name: "Room 1",
|
||||
},
|
||||
skey: "",
|
||||
}) as IStateEvent,
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
|
||||
?.content.name,
|
||||
).toEqual("Room 1");
|
||||
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
user: "alice",
|
||||
room: "!knock:bar",
|
||||
type: "m.room.name",
|
||||
content: {
|
||||
name: "Room 2",
|
||||
},
|
||||
skey: "",
|
||||
}) as IStateEvent,
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
|
||||
?.content.name,
|
||||
).toEqual("Room 2");
|
||||
});
|
||||
|
||||
it("should ignore state events in timeline", () => {
|
||||
const initState = {
|
||||
events: [member("alice", KnownMembership.Knock)],
|
||||
};
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
"org.matrix.msc4222.state_after": initState,
|
||||
}),
|
||||
);
|
||||
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
|
||||
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [],
|
||||
},
|
||||
"timeline": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
user: "alice",
|
||||
room: "!knock:bar",
|
||||
type: "m.room.name",
|
||||
content: {
|
||||
name: "Room 1",
|
||||
},
|
||||
skey: "",
|
||||
}) as IStateEvent,
|
||||
],
|
||||
prev_batch: "something",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
|
||||
?.content.name,
|
||||
).not.toEqual("Room 1");
|
||||
});
|
||||
|
||||
it("should not rewind state_after to start of timeline in toJSON", () => {
|
||||
const initState = {
|
||||
events: [member("alice", KnownMembership.Knock)],
|
||||
};
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
"org.matrix.msc4222.state_after": initState,
|
||||
"timeline": {
|
||||
events: initState.events,
|
||||
prev_batch: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
|
||||
|
||||
const joinEvent = member("alice", KnownMembership.Join);
|
||||
joinEvent.unsigned = { prev_content: initState.events[0].content, prev_sender: initState.events[0].sender };
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [joinEvent],
|
||||
},
|
||||
"timeline": {
|
||||
events: [joinEvent],
|
||||
prev_batch: "something",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const roomData = sa.getJSON().roomsData[Category.Join]["!foo:bar"];
|
||||
expect(roomData.state?.events.find((e) => e.type === "m.room.member")?.content.membership).toEqual(
|
||||
KnownMembership.Knock,
|
||||
);
|
||||
expect(
|
||||
roomData["org.matrix.msc4222.state_after"]?.events.find((e) => e.type === "m.room.member")?.content
|
||||
.membership,
|
||||
).toEqual(KnownMembership.Join);
|
||||
expect(roomData.timeline?.events.find((e) => e.type === "m.room.member")?.content.membership).toEqual(
|
||||
KnownMembership.Join,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function syncSkeleton(
|
||||
@@ -961,5 +1103,6 @@ function member(localpart: string, membership: Membership) {
|
||||
state_key: "@" + localpart + ":localhost",
|
||||
sender: "@" + localpart + ":localhost",
|
||||
type: "m.room.member",
|
||||
unsigned: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ function addEventsToTimeline(timeline: EventTimeline, numEvents: number, toStart
|
||||
user: USER_ID,
|
||||
event: true,
|
||||
}),
|
||||
{ toStartOfTimeline },
|
||||
{ toStartOfTimeline, addToState: false },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -451,8 +451,8 @@ describe("TimelineWindow", function () {
|
||||
const liveEvents = createEvents(5);
|
||||
const [, , e3, e4, e5] = oldEvents;
|
||||
const [, e7, e8, e9, e10] = liveEvents;
|
||||
room.addLiveEvents(liveEvents);
|
||||
room.addEventsToTimeline(oldEvents, true, oldTimeline);
|
||||
room.addLiveEvents(liveEvents, { addToState: false });
|
||||
room.addEventsToTimeline(oldEvents, true, false, oldTimeline);
|
||||
|
||||
// And 2 windows over the timelines in this room
|
||||
const oldWindow = new TimelineWindow(mockClient, timelineSet);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1430,7 +1430,7 @@ describe("Group Call", function () {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new MatrixClient({ baseUrl: "base_url" });
|
||||
client = new MatrixClient({ baseUrl: "base_url", userId: "my_user_id" });
|
||||
|
||||
jest.spyOn(client, "sendStateEvent").mockResolvedValue({} as any);
|
||||
});
|
||||
@@ -1566,16 +1566,19 @@ describe("Group Call", function () {
|
||||
async (roomId, eventType, content, stateKey) => {
|
||||
const eventId = `$${Math.random()}`;
|
||||
if (roomId === room.roomId) {
|
||||
room.addLiveEvents([
|
||||
new MatrixEvent({
|
||||
event_id: eventId,
|
||||
type: eventType,
|
||||
room_id: roomId,
|
||||
sender: FAKE_USER_ID_2,
|
||||
content,
|
||||
state_key: stateKey,
|
||||
}),
|
||||
]);
|
||||
room.addLiveEvents(
|
||||
[
|
||||
new MatrixEvent({
|
||||
event_id: eventId,
|
||||
type: eventType,
|
||||
room_id: roomId,
|
||||
sender: FAKE_USER_ID_2,
|
||||
content,
|
||||
state_key: stateKey,
|
||||
}),
|
||||
],
|
||||
{ addToState: true },
|
||||
);
|
||||
}
|
||||
return { event_id: eventId };
|
||||
},
|
||||
|
||||
Vendored
+21
-1
@@ -65,6 +65,26 @@ declare global {
|
||||
interface Navigator {
|
||||
// We check for the webkit-prefixed getUserMedia to detect if we're
|
||||
// on webkit: we should check if we still need to do this
|
||||
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
|
||||
webkitGetUserMedia?: DummyInterfaceWeShouldntBeUsingThis;
|
||||
}
|
||||
|
||||
export interface Uint8ArrayToBase64Options {
|
||||
alphabet?: "base64" | "base64url";
|
||||
omitPadding?: boolean;
|
||||
}
|
||||
|
||||
interface Uint8Array {
|
||||
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
|
||||
toBase64?(options?: Uint8ArrayToBase64Options): string;
|
||||
}
|
||||
|
||||
export interface Uint8ArrayFromBase64Options {
|
||||
alphabet?: "base64"; // Our fallback code only handles base64.
|
||||
lastChunkHandling?: "loose"; // Our fallback code doesn't support other handling at this time.
|
||||
}
|
||||
|
||||
interface Uint8ArrayConstructor {
|
||||
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64
|
||||
fromBase64?(base64: string, options?: Uint8ArrayFromBase64Options): Uint8Array;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ export class AutoDiscovery {
|
||||
* configuration, which may include error states. Rejects on unexpected
|
||||
* failure, not when verification fails.
|
||||
*/
|
||||
public static async fromDiscoveryConfig(wellknown: IClientWellKnown): Promise<ClientConfig> {
|
||||
public static async fromDiscoveryConfig(wellknown?: IClientWellKnown): Promise<ClientConfig> {
|
||||
// Step 1 is to get the config, which is provided to us here.
|
||||
|
||||
// We default to an error state to make the first few checks easier to
|
||||
|
||||
+38
-40
@@ -18,30 +18,32 @@ limitations under the License.
|
||||
* Base64 encoding and decoding utilities
|
||||
*/
|
||||
|
||||
function toBase64(uint8Array: Uint8Array, options: Uint8ArrayToBase64Options): string {
|
||||
if (typeof uint8Array.toBase64 === "function") {
|
||||
// Currently this is only supported in Firefox,
|
||||
// but we match the options in the hope in the future we can rely on it for all environments.
|
||||
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
|
||||
return uint8Array.toBase64(options);
|
||||
}
|
||||
|
||||
let base64 = btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), ""));
|
||||
if (options.omitPadding) {
|
||||
base64 = base64.replace(/={1,2}$/, "");
|
||||
}
|
||||
if (options.alphabet === "base64url") {
|
||||
base64 = base64.replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a typed array of uint8 as base64.
|
||||
* @param uint8Array - The data to encode.
|
||||
* @returns The base64.
|
||||
*/
|
||||
export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||
// A brief note on the state of base64 encoding in Javascript.
|
||||
// As of 2023, there is still no common native impl between both browsers and
|
||||
// node. Older Webpack provides an impl for Buffer and there is a polyfill class
|
||||
// for it. There are also plenty of pure js impls, eg. base64-js which has 2336
|
||||
// dependents at current count. Using this would probably be fine although it's
|
||||
// a little under-docced and run by an individual. The node impl works fine,
|
||||
// the browser impl works but predates Uint8Array and so only uses strings.
|
||||
// Right now, switching between native (or polyfilled) impls like this feels
|
||||
// like the least bad option, but... *shrugs*.
|
||||
if (typeof Buffer === "function") {
|
||||
return Buffer.from(uint8Array).toString("base64");
|
||||
} else if (typeof btoa === "function" && uint8Array instanceof Uint8Array) {
|
||||
// ArrayBuffer is a node concept so the param should always be a Uint8Array on
|
||||
// the browser. We need to check because ArrayBuffers don't have reduce.
|
||||
return btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), ""));
|
||||
} else {
|
||||
throw new Error("No base64 impl found!");
|
||||
}
|
||||
export function encodeBase64(uint8Array: Uint8Array): string {
|
||||
return toBase64(uint8Array, { alphabet: "base64", omitPadding: false });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,8 +51,8 @@ export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||
* @param uint8Array - The data to encode.
|
||||
* @returns The unpadded base64.
|
||||
*/
|
||||
export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||
return encodeBase64(uint8Array).replace(/={1,2}$/, "");
|
||||
export function encodeUnpaddedBase64(uint8Array: Uint8Array): string {
|
||||
return toBase64(uint8Array, { alphabet: "base64", omitPadding: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,8 +60,19 @@ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): stri
|
||||
* @param uint8Array - The data to encode.
|
||||
* @returns The unpadded base64.
|
||||
*/
|
||||
export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||
return encodeUnpaddedBase64(uint8Array).replace(/\+/g, "-").replace(/\//g, "_");
|
||||
export function encodeUnpaddedBase64Url(uint8Array: Uint8Array): string {
|
||||
return toBase64(uint8Array, { alphabet: "base64url", omitPadding: true });
|
||||
}
|
||||
|
||||
function fromBase64(base64: string, options: Uint8ArrayFromBase64Options): Uint8Array {
|
||||
if (typeof Uint8Array.fromBase64 === "function") {
|
||||
// Currently this is only supported in Firefox,
|
||||
// but we match the options in the hope in the future we can rely on it for all environments.
|
||||
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64
|
||||
return Uint8Array.fromBase64(base64, options);
|
||||
}
|
||||
|
||||
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,21 +81,6 @@ export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): s
|
||||
* @returns The decoded data.
|
||||
*/
|
||||
export function decodeBase64(base64: string): Uint8Array {
|
||||
// See encodeBase64 for a short treatise on base64 en/decoding in JS
|
||||
if (typeof Buffer === "function") {
|
||||
return Buffer.from(base64, "base64");
|
||||
} else if (typeof atob === "function") {
|
||||
const itFunc = function* (): Generator<number> {
|
||||
const decoded = atob(
|
||||
// built-in atob doesn't support base64url: convert so we support either
|
||||
base64.replace(/-/g, "+").replace(/_/g, "/"),
|
||||
);
|
||||
for (let i = 0; i < decoded.length; ++i) {
|
||||
yield decoded.charCodeAt(i);
|
||||
}
|
||||
};
|
||||
return Uint8Array.from(itFunc());
|
||||
} else {
|
||||
throw new Error("No base64 impl found!");
|
||||
}
|
||||
// The function requires us to select an alphabet, but we don't know if base64url was used so we convert.
|
||||
return fromBase64(base64.replace(/-/g, "+").replace(/_/g, "/"), { alphabet: "base64", lastChunkHandling: "loose" });
|
||||
}
|
||||
|
||||
+91
-40
@@ -46,7 +46,6 @@ import { noUnsafeEventProps, QueryDict, replaceParam, safeSet, sleep } from "./u
|
||||
import { Direction, EventTimeline } from "./models/event-timeline.ts";
|
||||
import { IActionsObject, PushProcessor } from "./pushprocessor.ts";
|
||||
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery.ts";
|
||||
import * as olmlib from "./crypto/olmlib.ts";
|
||||
import { decodeBase64, encodeBase64, encodeUnpaddedBase64Url } from "./base64.ts";
|
||||
import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice.ts";
|
||||
import { IOlmDevice } from "./crypto/algorithms/megolm.ts";
|
||||
@@ -80,7 +79,6 @@ import {
|
||||
CryptoEventHandlerMap as LegacyCryptoEventHandlerMap,
|
||||
fixBackupKey,
|
||||
ICheckOwnCrossSigningTrustOpts,
|
||||
ICryptoCallbacks,
|
||||
IRoomKeyRequestBody,
|
||||
isCryptoAvailable,
|
||||
} from "./crypto/index.ts";
|
||||
@@ -229,6 +227,7 @@ import {
|
||||
ImportRoomKeysOpts,
|
||||
CryptoEvent,
|
||||
CryptoEventHandlerMap,
|
||||
CryptoCallbacks,
|
||||
} from "./crypto-api/index.ts";
|
||||
import { DeviceInfoMap } from "./crypto/DeviceList.ts";
|
||||
import {
|
||||
@@ -252,6 +251,9 @@ export type Store = IStore;
|
||||
export type ResetTimelineCallback = (roomId: string) => boolean;
|
||||
|
||||
const SCROLLBACK_DELAY_MS = 3000;
|
||||
/**
|
||||
* @deprecated Not supported for Rust Cryptography.
|
||||
*/
|
||||
export const CRYPTO_ENABLED: boolean = isCryptoAvailable();
|
||||
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
|
||||
|
||||
@@ -286,12 +288,16 @@ export interface ICreateClientOpts {
|
||||
store?: Store;
|
||||
|
||||
/**
|
||||
* A store to be used for end-to-end crypto session data. If not specified,
|
||||
* end-to-end crypto will be disabled. The `createClient` helper will create
|
||||
* a default store if needed. Calls the factory supplied to
|
||||
* A store to be used for end-to-end crypto session data.
|
||||
* The `createClient` helper will create a default store if needed. Calls the factory supplied to
|
||||
* {@link setCryptoStoreFactory} if unspecified; or if no factory has been
|
||||
* specified, uses a default implementation (indexeddb in the browser,
|
||||
* in-memory otherwise).
|
||||
*
|
||||
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initLegacyCrypto}),
|
||||
* but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device
|
||||
* previously used legacy crypto (so must be migrated), then this must still be provided, so that the
|
||||
* data can be migrated from the legacy store.
|
||||
*/
|
||||
cryptoStore?: CryptoStore;
|
||||
|
||||
@@ -383,7 +389,7 @@ export interface ICreateClientOpts {
|
||||
*
|
||||
* This must be set to the same value every time the client is initialised for the same device.
|
||||
*
|
||||
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initCrypto}),
|
||||
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initLegacyCrypto}),
|
||||
* but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device
|
||||
* previously used legacy crypto (so must be migrated), then this must still be provided, so that the
|
||||
* data can be migrated from the legacy store.
|
||||
@@ -433,7 +439,7 @@ export interface ICreateClientOpts {
|
||||
/**
|
||||
* Crypto callbacks provided by the application
|
||||
*/
|
||||
cryptoCallbacks?: ICryptoCallbacks;
|
||||
cryptoCallbacks?: CryptoCallbacks;
|
||||
|
||||
/**
|
||||
* Method to generate room names for empty rooms and rooms names based on membership.
|
||||
@@ -1216,7 +1222,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
private readonly logger: Logger;
|
||||
|
||||
public reEmitter = new TypedReEmitter<EmittedEvents, ClientEventHandlerMap>(this);
|
||||
public olmVersion: [number, number, number] | null = null; // populated after initCrypto
|
||||
public olmVersion: [number, number, number] | null = null; // populated after initLegacyCrypto
|
||||
public usingExternalCrypto = false;
|
||||
private _store!: Store;
|
||||
public deviceId: string | null;
|
||||
@@ -1249,7 +1255,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public crypto?: Crypto; // XXX: Intended private, used in code. Being replaced by cryptoBackend
|
||||
|
||||
private cryptoBackend?: CryptoBackend; // one of crypto or rustCrypto
|
||||
public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code.
|
||||
public cryptoCallbacks: CryptoCallbacks; // XXX: Intended private, used in code.
|
||||
public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code.
|
||||
public groupCallEventHandler?: GroupCallEventHandler;
|
||||
public supportsCallTransfer = false; // XXX: Intended private, used in code.
|
||||
@@ -1269,6 +1275,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
protected isGuestAccount = false;
|
||||
protected ongoingScrollbacks: { [roomId: string]: { promise?: Promise<Room>; errorTs?: number } } = {};
|
||||
protected notifTimelineSet: EventTimelineSet | null = null;
|
||||
/* @deprecated */
|
||||
protected cryptoStore?: CryptoStore;
|
||||
protected verificationMethods?: string[];
|
||||
protected fallbackICEServerAllowed = false;
|
||||
@@ -1598,7 +1605,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* Try to rehydrate a device if available. The client must have been
|
||||
* initialized with a `cryptoCallback.getDehydrationKey` option, and this
|
||||
* function must be called before initCrypto and startClient are called.
|
||||
* function must be called before initLegacyCrypto and startClient are called.
|
||||
*
|
||||
* @returns Promise which resolves to undefined if a device could not be dehydrated, or
|
||||
* to the new device ID if the dehydration was successful.
|
||||
@@ -1825,10 +1832,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns MXID for the logged-in user, or null if not logged in
|
||||
*/
|
||||
public getUserId(): string | null {
|
||||
if (this.credentials && this.credentials.userId) {
|
||||
return this.credentials.userId;
|
||||
}
|
||||
return null;
|
||||
return this.credentials?.userId ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1850,7 +1854,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Domain of this MXID
|
||||
*/
|
||||
public getDomain(): string | null {
|
||||
if (this.credentials && this.credentials.userId) {
|
||||
if (this.credentials?.userId) {
|
||||
return this.credentials.userId.replace(/^.*?:/, "");
|
||||
}
|
||||
return null;
|
||||
@@ -1861,10 +1865,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns The user ID localpart or null.
|
||||
*/
|
||||
public getUserIdLocalpart(): string | null {
|
||||
if (this.credentials && this.credentials.userId) {
|
||||
return this.credentials.userId.split(":")[0].substring(1);
|
||||
}
|
||||
return null;
|
||||
return this.credentials?.userId?.split(":")[0].substring(1) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2146,7 +2147,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @deprecated libolm is deprecated. Prefer {@link initRustCrypto}.
|
||||
*/
|
||||
public async initCrypto(): Promise<void> {
|
||||
public async initLegacyCrypto(): Promise<void> {
|
||||
if (!isCryptoAvailable()) {
|
||||
throw new Error(
|
||||
`End-to-end encryption not supported in this js-sdk build: did ` +
|
||||
@@ -2219,7 +2220,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* Initialise support for end-to-end encryption in this client, using the rust matrix-sdk-crypto.
|
||||
*
|
||||
* An alternative to {@link initCrypto}.
|
||||
* An alternative to {@link initLegacyCrypto}.
|
||||
*
|
||||
* **WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to
|
||||
* the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for
|
||||
* ensuring that only one `MatrixClient` issue is instantiated at a time.
|
||||
*
|
||||
* @param args.useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'.
|
||||
* @param args.storageKey - A key with which to encrypt the indexeddb store. If provided, it must be exactly
|
||||
@@ -2316,7 +2321,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* Access the crypto API for this client.
|
||||
*
|
||||
* If end-to-end encryption has been enabled for this client (via {@link initCrypto} or {@link initRustCrypto}),
|
||||
* If end-to-end encryption has been enabled for this client (via {@link initLegacyCrypto} or {@link initRustCrypto}),
|
||||
* returns an object giving access to the crypto API. Otherwise, returns `undefined`.
|
||||
*/
|
||||
public getCrypto(): CryptoApi | undefined {
|
||||
@@ -2425,6 +2430,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @remarks
|
||||
* Fires {@link CryptoEvent#DeviceVerificationChanged}
|
||||
*
|
||||
* @deprecated Not supported for Rust Cryptography.
|
||||
*/
|
||||
public setDeviceVerified(userId: string, deviceId: string, verified = true): Promise<void> {
|
||||
const prom = this.setDeviceVerification(userId, deviceId, verified, null, null);
|
||||
@@ -3152,6 +3159,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @returns true if the sender of this event has been verified using
|
||||
* {@link MatrixClient#setDeviceVerified}.
|
||||
*
|
||||
* @deprecated Not supported for Rust Cryptography.
|
||||
*/
|
||||
public async isEventSenderVerified(event: MatrixEvent): Promise<boolean> {
|
||||
const device = await this.getEventSenderDeviceInfo(event);
|
||||
@@ -3352,7 +3361,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @returns Information object from API, or null if no backup is present on the server.
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.getActiveSessionBackupVersion}.
|
||||
* @deprecated Prefer {@link CryptoApi.getKeyBackupInfo}.
|
||||
*/
|
||||
public async getKeyBackupVersion(): Promise<IKeyBackupInfo | null> {
|
||||
let res: IKeyBackupInfo;
|
||||
@@ -3698,6 +3707,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param opts - Optional params such as callbacks
|
||||
* @returns Status of restoration with `total` and `imported`
|
||||
* key counts.
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
|
||||
*/
|
||||
public async restoreKeyBackupWithPassword(
|
||||
password: string,
|
||||
@@ -3706,6 +3717,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
/**
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
|
||||
*/
|
||||
public async restoreKeyBackupWithPassword(
|
||||
password: string,
|
||||
targetRoomId: string,
|
||||
@@ -3713,6 +3727,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
/**
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
|
||||
*/
|
||||
public async restoreKeyBackupWithPassword(
|
||||
password: string,
|
||||
targetRoomId: string,
|
||||
@@ -3720,6 +3737,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
/**
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
|
||||
*/
|
||||
public async restoreKeyBackupWithPassword(
|
||||
password: string,
|
||||
targetRoomId: string | undefined,
|
||||
@@ -3743,6 +3763,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param opts - Optional params such as callbacks
|
||||
* @returns Status of restoration with `total` and `imported`
|
||||
* key counts.
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||
*/
|
||||
public async restoreKeyBackupWithSecretStorage(
|
||||
backupInfo: IKeyBackupInfo,
|
||||
@@ -3780,6 +3802,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
* @returns Status of restoration with `total` and `imported`
|
||||
* key counts.
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||
*/
|
||||
public restoreKeyBackupWithRecoveryKey(
|
||||
recoveryKey: string,
|
||||
@@ -3788,6 +3812,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
/**
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||
*/
|
||||
public restoreKeyBackupWithRecoveryKey(
|
||||
recoveryKey: string,
|
||||
targetRoomId: string,
|
||||
@@ -3795,6 +3822,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
/**
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||
*/
|
||||
public restoreKeyBackupWithRecoveryKey(
|
||||
recoveryKey: string,
|
||||
targetRoomId: string,
|
||||
@@ -3802,6 +3832,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
/**
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||
*/
|
||||
public restoreKeyBackupWithRecoveryKey(
|
||||
recoveryKey: string,
|
||||
targetRoomId: string | undefined,
|
||||
@@ -3813,24 +3846,42 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from an existing key backup via a private key stored locally
|
||||
* @param targetRoomId
|
||||
* @param targetSessionId
|
||||
* @param backupInfo
|
||||
* @param opts
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||
*/
|
||||
public async restoreKeyBackupWithCache(
|
||||
targetRoomId: undefined,
|
||||
targetSessionId: undefined,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
/**
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||
*/
|
||||
public async restoreKeyBackupWithCache(
|
||||
targetRoomId: string,
|
||||
targetSessionId: undefined,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
/**
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||
*/
|
||||
public async restoreKeyBackupWithCache(
|
||||
targetRoomId: string,
|
||||
targetSessionId: string,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
/**
|
||||
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
|
||||
*/
|
||||
public async restoreKeyBackupWithCache(
|
||||
targetRoomId: string | undefined,
|
||||
targetSessionId: string | undefined,
|
||||
@@ -3848,28 +3899,28 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
private async restoreKeyBackup(
|
||||
privKey: ArrayLike<number>,
|
||||
privKey: Uint8Array,
|
||||
targetRoomId: undefined,
|
||||
targetSessionId: undefined,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
private async restoreKeyBackup(
|
||||
privKey: ArrayLike<number>,
|
||||
privKey: Uint8Array,
|
||||
targetRoomId: string,
|
||||
targetSessionId: undefined,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
private async restoreKeyBackup(
|
||||
privKey: ArrayLike<number>,
|
||||
privKey: Uint8Array,
|
||||
targetRoomId: string,
|
||||
targetSessionId: string,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult>;
|
||||
private async restoreKeyBackup(
|
||||
privKey: ArrayLike<number>,
|
||||
privKey: Uint8Array,
|
||||
targetRoomId: string | undefined,
|
||||
targetSessionId: string | undefined,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
@@ -4260,7 +4311,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public getIgnoredUsers(): string[] {
|
||||
const event = this.getAccountData("m.ignored_user_list");
|
||||
if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return [];
|
||||
if (!event?.getContent()["ignored_users"]) return [];
|
||||
return Object.keys(event.getContent()["ignored_users"]);
|
||||
}
|
||||
|
||||
@@ -6087,7 +6138,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
room.partitionThreadedEvents(matrixEvents);
|
||||
|
||||
this.processAggregatedTimelineEvents(room, timelineEvents);
|
||||
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||
room.addEventsToTimeline(timelineEvents, true, true, room.getLiveTimeline());
|
||||
this.processThreadEvents(room, threadedEvents, true);
|
||||
unknownRelations.forEach((event) => room.relations.aggregateChildEvent(event));
|
||||
|
||||
@@ -6199,7 +6250,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
const [timelineEvents, threadedEvents, unknownRelations] = timelineSet.room.partitionThreadedEvents(events);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, true, false, timeline, res.start);
|
||||
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
|
||||
this.processThreadEvents(timelineSet.room, threadedEvents, true);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents);
|
||||
@@ -6293,10 +6344,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
timeline.initialiseState(res.state.map(mapper));
|
||||
}
|
||||
|
||||
timelineSet.addEventsToTimeline(events, true, timeline, resNewer.next_batch);
|
||||
timelineSet.addEventsToTimeline(events, true, false, timeline, resNewer.next_batch);
|
||||
if (!resOlder.next_batch) {
|
||||
const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
|
||||
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
|
||||
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, false, timeline, null);
|
||||
}
|
||||
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
|
||||
timeline.setPaginationToken(resNewer.next_batch ?? null, Direction.Forward);
|
||||
@@ -6350,10 +6401,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const timeline = timelineSet.getLiveTimeline();
|
||||
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
|
||||
|
||||
timelineSet.addEventsToTimeline(events, true, timeline, null);
|
||||
timelineSet.addEventsToTimeline(events, true, false, timeline, null);
|
||||
if (!resOlder.next_batch) {
|
||||
const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
|
||||
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
|
||||
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, false, timeline, null);
|
||||
}
|
||||
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
|
||||
timeline.setPaginationToken(null, Direction.Forward);
|
||||
@@ -6616,7 +6667,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// No need to partition events for threads here, everything lives
|
||||
// in the notification timeline set
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, token);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
@@ -6659,7 +6710,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
|
||||
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, token);
|
||||
this.processAggregatedTimelineEvents(room, matrixEvents);
|
||||
this.processThreadRoots(room, matrixEvents, backwards);
|
||||
|
||||
@@ -6707,12 +6758,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const newToken = res.next_batch;
|
||||
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, newToken ?? null);
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, newToken ?? null);
|
||||
if (!newToken && backwards) {
|
||||
const originalEvent =
|
||||
thread.rootEvent ??
|
||||
mapper(await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id));
|
||||
timelineSet.addEventsToTimeline([originalEvent], true, eventTimeline, null);
|
||||
timelineSet.addEventsToTimeline([originalEvent], true, false, eventTimeline, null);
|
||||
}
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
|
||||
|
||||
@@ -6751,7 +6802,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
const [timelineEvents, , unknownRelations] = room.partitionThreadedEvents(matrixEvents);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, false, eventTimeline, token);
|
||||
this.processAggregatedTimelineEvents(room, timelineEvents);
|
||||
this.processThreadRoots(
|
||||
room,
|
||||
@@ -10066,7 +10117,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
type: EventType.RoomEncryption,
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -78,6 +78,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
* Get information about the encryption of an event
|
||||
*
|
||||
* @param event - event to be checked
|
||||
* @deprecated Use {@link CryptoApi#getEncryptionInfoForEvent} instead
|
||||
*/
|
||||
getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo;
|
||||
|
||||
@@ -107,7 +108,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
* @param backupInfo - The backup information
|
||||
* @param privKey - The private decryption key.
|
||||
*/
|
||||
getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor>;
|
||||
getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise<BackupDecryptor>;
|
||||
|
||||
/**
|
||||
* Import a list of room keys restored from backup
|
||||
|
||||
@@ -29,4 +29,5 @@ export type CryptoEventHandlerMap = {
|
||||
[CryptoEvent.KeysChanged]: (data: {}) => void;
|
||||
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void;
|
||||
} & RustBackupCryptoEventMap;
|
||||
|
||||
+73
-3
@@ -22,7 +22,13 @@ import { DeviceMap } from "../models/device.ts";
|
||||
import { UIAuthCallback } from "../interactive-auth.ts";
|
||||
import { PassphraseInfo, SecretStorageCallbacks, SecretStorageKeyDescription } from "../secret-storage.ts";
|
||||
import { VerificationRequest } from "./verification.ts";
|
||||
import { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "./keybackup.ts";
|
||||
import {
|
||||
BackupTrustInfo,
|
||||
KeyBackupCheck,
|
||||
KeyBackupInfo,
|
||||
KeyBackupRestoreOpts,
|
||||
KeyBackupRestoreResult,
|
||||
} from "./keybackup.ts";
|
||||
import { ISignatures } from "../@types/signed.ts";
|
||||
import { MatrixEvent } from "../models/event.ts";
|
||||
|
||||
@@ -502,6 +508,18 @@ export interface CryptoApi {
|
||||
*/
|
||||
storeSessionBackupPrivateKey(key: Uint8Array, version: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Attempt to fetch the backup decryption key from secret storage.
|
||||
*
|
||||
* If the key is found in secret storage, checks it against the latest backup on the server;
|
||||
* if they match, stores the key in the crypto store by calling {@link storeSessionBackupPrivateKey},
|
||||
* which enables automatic restore of individual keys when an Unable-to-decrypt error is encountered.
|
||||
*
|
||||
* If we are unable to fetch the key from secret storage, there is no backup on the server, or the key
|
||||
* does not match, throws an exception.
|
||||
*/
|
||||
loadSessionBackupPrivateKeyFromSecretStorage(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the current status of key backup.
|
||||
*
|
||||
@@ -516,6 +534,18 @@ export interface CryptoApi {
|
||||
*/
|
||||
isKeyBackupTrusted(info: KeyBackupInfo): Promise<BackupTrustInfo>;
|
||||
|
||||
/**
|
||||
* Return the details of the latest backup on the server, when we last checked.
|
||||
*
|
||||
* This normally returns a cached value, but if we haven't yet made a request to the server, it will fire one off.
|
||||
* It will always return the details of the active backup if key backup is enabled.
|
||||
*
|
||||
* Return null if there is no backup.
|
||||
*
|
||||
* @returns the key backup information
|
||||
*/
|
||||
getKeyBackupInfo(): Promise<KeyBackupInfo | null>;
|
||||
|
||||
/**
|
||||
* Force a re-check of the key backup and enable/disable it as appropriate.
|
||||
*
|
||||
@@ -545,6 +575,36 @@ export interface CryptoApi {
|
||||
*/
|
||||
deleteKeyBackupVersion(version: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Download and restore the full key backup from the homeserver.
|
||||
*
|
||||
* Before calling this method, a decryption key, and the backup version to restore,
|
||||
* must have been saved in the crypto store. This happens in one of the following ways:
|
||||
*
|
||||
* - When a new backup version is created with {@link CryptoApi.resetKeyBackup}, a new key is created and cached.
|
||||
* - The key can be loaded from secret storage with {@link CryptoApi.loadSessionBackupPrivateKeyFromSecretStorage}.
|
||||
* - The key can be received from another device via secret sharing, typically as part of the interactive verification flow.
|
||||
* - The key and backup version can also be set explicitly via {@link CryptoApi.storeSessionBackupPrivateKey},
|
||||
* though this is not expected to be a common operation.
|
||||
*
|
||||
* Warning: the full key backup may be quite large, so this operation may take several hours to complete.
|
||||
* Use of {@link KeyBackupRestoreOpts.progressCallback} is recommended.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
restoreKeyBackup(opts?: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult>;
|
||||
|
||||
/**
|
||||
* Restores a key backup using a passphrase.
|
||||
* The decoded key (derived from the passphrase) is stored locally by calling {@link CryptoApi#storeSessionBackupPrivateKey}.
|
||||
*
|
||||
* @param passphrase - The passphrase to use to restore the key backup.
|
||||
* @param opts
|
||||
*
|
||||
* @deprecated Deriving a backup key from a passphrase is not part of the matrix spec. Instead, a random key is generated and stored/shared via 4S.
|
||||
*/
|
||||
restoreKeyBackupWithPassphrase(passphrase: string, opts?: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult>;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Dehydrated devices
|
||||
@@ -886,8 +946,8 @@ export class DeviceVerificationStatus {
|
||||
|
||||
/**
|
||||
* Room key import progress report.
|
||||
* Used when calling {@link CryptoApi#importRoomKeys} or
|
||||
* {@link CryptoApi#importRoomKeysAsJson} as the parameter of
|
||||
* Used when calling {@link CryptoApi#importRoomKeys},
|
||||
* {@link CryptoApi#importRoomKeysAsJson} or {@link CryptoApi#restoreKeyBackup} as the parameter of
|
||||
* the progressCallback. Used to display feedback.
|
||||
*/
|
||||
export interface ImportRoomKeyProgressData {
|
||||
@@ -1097,6 +1157,16 @@ export enum EventShieldReason {
|
||||
* decryption keys.
|
||||
*/
|
||||
MISMATCHED_SENDER_KEY,
|
||||
|
||||
/**
|
||||
* The event was sent unencrypted in an encrypted room.
|
||||
*/
|
||||
SENT_IN_CLEAR,
|
||||
|
||||
/**
|
||||
* The sender was previously verified but changed their identity.
|
||||
*/
|
||||
VERIFICATION_VIOLATION,
|
||||
}
|
||||
|
||||
/** The result of a call to {@link CryptoApi.getOwnDeviceKeys} */
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import { ISigned } from "../@types/signed.ts";
|
||||
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||
import { ImportRoomKeyProgressData } from "./index.ts";
|
||||
|
||||
export interface Curve25519AuthData {
|
||||
public_key: string;
|
||||
@@ -87,3 +88,28 @@ export interface KeyBackupSession<T = Curve25519SessionData | AESEncryptedSecret
|
||||
export interface KeyBackupRoomSessions {
|
||||
[sessionId: string]: KeyBackupSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra parameters for {@link CryptoApi.restoreKeyBackup} and {@link CryptoApi.restoreKeyBackupWithPassphrase}.
|
||||
*/
|
||||
export interface KeyBackupRestoreOpts {
|
||||
/**
|
||||
* A callback which, if defined, will be called periodically to report ongoing progress of the backup restore process.
|
||||
* @param progress
|
||||
*/
|
||||
progressCallback?: (progress: ImportRoomKeyProgressData) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of {@link CryptoApi.restoreKeyBackup}.
|
||||
*/
|
||||
export interface KeyBackupRestoreResult {
|
||||
/**
|
||||
* The total number of keys that were found in the backup.
|
||||
*/
|
||||
total: number;
|
||||
/**
|
||||
* The number of keys that were imported.
|
||||
*/
|
||||
imported: number;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const KEY_SIZE = 32;
|
||||
* @param key
|
||||
*/
|
||||
export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
|
||||
const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
|
||||
const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
|
||||
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
|
||||
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ export interface VerificationRequest
|
||||
* @param qrCodeData - the decoded QR code.
|
||||
* @returns A verifier; call `.verify()` on it to wait for the other side to complete the verification flow.
|
||||
*/
|
||||
scanQRCode(qrCodeData: Uint8Array): Promise<Verifier>;
|
||||
scanQRCode(qrCodeData: Uint8ClampedArray): Promise<Verifier>;
|
||||
|
||||
/**
|
||||
* The verifier which is doing the actual verification, once the method has been established.
|
||||
@@ -170,7 +170,7 @@ export interface VerificationRequest
|
||||
*
|
||||
* @deprecated Not supported in Rust Crypto. Use {@link VerificationRequest#generateQRCode} instead.
|
||||
*/
|
||||
getQRCodeBytes(): Buffer | undefined;
|
||||
getQRCodeBytes(): Uint8ClampedArray | undefined;
|
||||
|
||||
/**
|
||||
* Generate the data for a QR code allowing the other device to verify this one, if it supports it.
|
||||
@@ -178,7 +178,7 @@ export interface VerificationRequest
|
||||
* Only returns data once `phase` is {@link VerificationPhase.Ready} and the other party can scan a QR code;
|
||||
* otherwise returns `undefined`.
|
||||
*/
|
||||
generateQRCode(): Promise<Buffer | undefined>;
|
||||
generateQRCode(): Promise<Uint8ClampedArray | undefined>;
|
||||
|
||||
/**
|
||||
* If this request has been cancelled, the cancellation code (e.g `m.user`) which is responsible for cancelling
|
||||
|
||||
@@ -106,7 +106,7 @@ export class EncryptionSetupBuilder {
|
||||
if (!this.keySignatures) {
|
||||
this.keySignatures = {};
|
||||
}
|
||||
const userSignatures = this.keySignatures[userId] || {};
|
||||
const userSignatures = this.keySignatures[userId] ?? {};
|
||||
this.keySignatures[userId] = userSignatures;
|
||||
userSignatures[deviceId] = signature;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts
|
||||
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
|
||||
|
||||
export interface IDehydratedDevice {
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
device_data: SecretStorageKeyDescription & {
|
||||
device_id?: string; // eslint-disable-line camelcase
|
||||
device_data?: SecretStorageKeyDescription & {
|
||||
// eslint-disable-line camelcase
|
||||
algorithm: string;
|
||||
account: string; // pickle
|
||||
@@ -90,7 +90,7 @@ export class DehydrationManager {
|
||||
}
|
||||
|
||||
public async setKey(
|
||||
key: Uint8Array,
|
||||
key?: Uint8Array,
|
||||
keyInfo: { [props: string]: any } = {},
|
||||
deviceDisplayName?: string,
|
||||
): Promise<boolean | undefined> {
|
||||
|
||||
+34
-3
@@ -102,6 +102,8 @@ import {
|
||||
OwnDeviceKeys,
|
||||
CryptoEvent as CryptoApiCryptoEvent,
|
||||
CryptoEventHandlerMap as CryptoApiCryptoEventHandlerMap,
|
||||
KeyBackupRestoreResult,
|
||||
KeyBackupRestoreOpts,
|
||||
} from "../crypto-api/index.ts";
|
||||
import { Device, DeviceMap } from "../models/device.ts";
|
||||
import { deviceInfoToDevice } from "./device-converter.ts";
|
||||
@@ -324,8 +326,6 @@ export type CryptoEventHandlerMap = CryptoApiCryptoEventHandlerMap & {
|
||||
*/
|
||||
[CryptoEvent.Warning]: (type: string) => void;
|
||||
[CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void;
|
||||
|
||||
[CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void;
|
||||
};
|
||||
|
||||
export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap> implements CryptoBackend {
|
||||
@@ -1306,6 +1306,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link Crypto.loadSessionBackupPrivateKeyFromSecretStorage}.
|
||||
*/
|
||||
public loadSessionBackupPrivateKeyFromSecretStorage(): Promise<void> {
|
||||
throw new Error("Not implmeented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status of key backup.
|
||||
*
|
||||
@@ -1318,6 +1325,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link Crypto.CryptoApi#getKeyBackupInfo}.
|
||||
*/
|
||||
public async getKeyBackupInfo(): Promise<KeyBackupInfo | null> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a key backup can be trusted.
|
||||
*
|
||||
@@ -1832,7 +1846,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
|
||||
*/
|
||||
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
|
||||
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise<BackupDecryptor> {
|
||||
if (!(privKey instanceof Uint8Array)) {
|
||||
throw new Error(`getBackupDecryptor expects Uint8Array`);
|
||||
}
|
||||
@@ -4308,6 +4322,23 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
public async startDehydration(createNewKey?: boolean): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub function -- restoreKeyBackup is not implemented here, so throw error
|
||||
*/
|
||||
public restoreKeyBackup(opts: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub function -- restoreKeyBackupWithPassphrase is not implemented here, so throw error
|
||||
*/
|
||||
public restoreKeyBackupWithPassphrase(
|
||||
passphrase: string,
|
||||
opts: KeyBackupRestoreOpts,
|
||||
): Promise<KeyBackupRestoreResult> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -162,7 +162,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
func: (session: ISessionInfo) => void,
|
||||
): void {
|
||||
const sessions = this._getEndToEndSessions(deviceKey);
|
||||
func(sessions[sessionId] || {});
|
||||
func(sessions[sessionId] ?? {});
|
||||
}
|
||||
|
||||
public getEndToEndSessions(
|
||||
@@ -170,7 +170,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
txn: unknown,
|
||||
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
|
||||
): void {
|
||||
func(this._getEndToEndSessions(deviceKey) || {});
|
||||
func(this._getEndToEndSessions(deviceKey) ?? {});
|
||||
}
|
||||
|
||||
public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
|
||||
|
||||
@@ -281,8 +281,9 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
*
|
||||
* @deprecated Prefer `generateQRCode`.
|
||||
*/
|
||||
public getQRCodeBytes(): Buffer | undefined {
|
||||
return this._qrCodeData?.getBuffer();
|
||||
public getQRCodeBytes(): Uint8ClampedArray | undefined {
|
||||
if (!this._qrCodeData) return;
|
||||
return new Uint8ClampedArray(this._qrCodeData.getBuffer());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,7 +292,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
* Only returns data once `phase` is `Ready` and the other party can scan a QR code;
|
||||
* otherwise returns `undefined`.
|
||||
*/
|
||||
public async generateQRCode(): Promise<Buffer | undefined> {
|
||||
public async generateQRCode(): Promise<Uint8ClampedArray | undefined> {
|
||||
return this.getQRCodeBytes();
|
||||
}
|
||||
|
||||
@@ -478,7 +479,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
return verifier;
|
||||
}
|
||||
|
||||
public scanQRCode(qrCodeData: Uint8Array): Promise<Verifier> {
|
||||
public scanQRCode(qrCodeData: Uint8ClampedArray): Promise<Verifier> {
|
||||
throw new Error("QR code scanning not supported by legacy crypto");
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -18,11 +18,11 @@ limitations under the License.
|
||||
* Computes a SHA-256 hash of a string (after utf-8 encoding) and returns it as an ArrayBuffer.
|
||||
*
|
||||
* @param plaintext The string to hash
|
||||
* @returns An ArrayBuffer containing the SHA-256 hash of the input string
|
||||
* @returns An Uint8Array containing the SHA-256 hash of the input string
|
||||
* @throws If the subtle crypto API is not available, for example if the code is running
|
||||
* in a web page with an insecure context (eg. served over plain HTTP).
|
||||
*/
|
||||
export async function sha256(plaintext: string): Promise<ArrayBuffer> {
|
||||
export async function sha256(plaintext: string): Promise<Uint8Array> {
|
||||
if (!globalThis.crypto.subtle) {
|
||||
throw new Error("Crypto.subtle is not available: insecure context?");
|
||||
}
|
||||
@@ -30,5 +30,5 @@ export async function sha256(plaintext: string): Promise<ArrayBuffer> {
|
||||
|
||||
const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8);
|
||||
|
||||
return digest;
|
||||
return new Uint8Array(digest);
|
||||
}
|
||||
|
||||
+101
-3
@@ -57,6 +57,7 @@ import { ToDeviceBatch, ToDevicePayload } from "./models/ToDeviceMessage.ts";
|
||||
import { DeviceInfo } from "./crypto/deviceinfo.ts";
|
||||
import { IOlmDevice } from "./crypto/algorithms/megolm.ts";
|
||||
import { MapWithDefault, recursiveMapToObject } from "./utils.ts";
|
||||
import { TypedEventEmitter } from "./matrix.ts";
|
||||
|
||||
interface IStateEventRequest {
|
||||
eventType: string;
|
||||
@@ -123,6 +124,10 @@ export interface ICapabilities {
|
||||
updateDelayedEvents?: boolean;
|
||||
}
|
||||
|
||||
export enum RoomWidgetClientEvent {
|
||||
PendingEventsChanged = "PendingEvent.pendingEventsChanged",
|
||||
}
|
||||
export type EventHandlerMap = { [RoomWidgetClientEvent.PendingEventsChanged]: () => void };
|
||||
/**
|
||||
* A MatrixClient that routes its requests through the widget API instead of the
|
||||
* real CS API.
|
||||
@@ -134,6 +139,9 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
private lifecycle?: AbortController;
|
||||
private syncState: SyncState | null = null;
|
||||
|
||||
private pendingSendingEventsTxId: { type: string; id: string | undefined; txId: string }[] = [];
|
||||
private eventEmitter = new TypedEventEmitter<keyof EventHandlerMap, EventHandlerMap>();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param widgetApi - The widget api to use for communication.
|
||||
@@ -276,7 +284,13 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
|
||||
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
|
||||
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], events);
|
||||
if (this.syncApi instanceof SyncApi) {
|
||||
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
|
||||
// -> state events in `timelineEventList` will update the state.
|
||||
await this.syncApi.injectRoomEvents(this.room!, undefined, events);
|
||||
} else {
|
||||
await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync
|
||||
}
|
||||
events.forEach((event) => {
|
||||
this.emit(ClientEvent.Event, event);
|
||||
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
||||
@@ -330,6 +344,8 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
const content = event.event.redacts
|
||||
? { ...event.getContent(), redacts: event.event.redacts }
|
||||
: event.getContent();
|
||||
|
||||
// Delayed event special case.
|
||||
if (delayOpts) {
|
||||
// TODO: updatePendingEvent for delayed events?
|
||||
const response = await this.widgetApi.sendRoomEvent(
|
||||
@@ -342,6 +358,10 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
return this.validateSendDelayedEventResponse(response);
|
||||
}
|
||||
|
||||
const txId = event.getTxnId();
|
||||
// Add the txnId to the pending list (still with unknown evID)
|
||||
if (txId) this.pendingSendingEventsTxId.push({ type: event.getType(), id: undefined, txId });
|
||||
|
||||
let response: ISendEventFromWidgetResponseData;
|
||||
try {
|
||||
response = await this.widgetApi.sendRoomEvent(event.getType(), content, room.roomId);
|
||||
@@ -349,9 +369,15 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// This also checks for an event id on the response
|
||||
room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
|
||||
|
||||
// Update the pending events list with the eventId
|
||||
this.pendingSendingEventsTxId.forEach((p) => {
|
||||
if (p.txId === txId) p.id = response.event_id;
|
||||
});
|
||||
this.eventEmitter.emit(RoomWidgetClientEvent.PendingEventsChanged);
|
||||
|
||||
return { event_id: response.event_id! };
|
||||
}
|
||||
|
||||
@@ -495,6 +521,48 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
await this.widgetApi.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
|
||||
}
|
||||
|
||||
private updateTxId = async (event: MatrixEvent): Promise<void> => {
|
||||
// We update the txId for remote echos that originate from this client.
|
||||
// This happens with the help of `pendingSendingEventsTxId` where we store all events that are currently sending
|
||||
// with their widget txId and once ready the final evId.
|
||||
if (
|
||||
// This could theoretically be an event send by this device
|
||||
// In that case we need to update the txId of the event because the embedded client/widget
|
||||
// knows this event with a different transaction Id than what was used by the host client.
|
||||
event.getSender() === this.getUserId() &&
|
||||
// We optimize by not blocking events from types that we have not send
|
||||
// with this client.
|
||||
this.pendingSendingEventsTxId.some((p) => event.getType() === p.type)
|
||||
) {
|
||||
// Compare by event Id if we have a matching pending event where we know the txId.
|
||||
let matchingTxId = this.pendingSendingEventsTxId.find((p) => p.id === event.getId())?.txId;
|
||||
// Block any further processing of this event until we have received the sending response.
|
||||
// -> until we know the event id.
|
||||
// -> until we have not pending events anymore.
|
||||
while (!matchingTxId && this.pendingSendingEventsTxId.length > 0) {
|
||||
// Recheck whenever the PendingEventsChanged
|
||||
await new Promise<void>((resolve) =>
|
||||
this.eventEmitter.once(RoomWidgetClientEvent.PendingEventsChanged, () => resolve()),
|
||||
);
|
||||
matchingTxId = this.pendingSendingEventsTxId.find((p) => p.id === event.getId())?.txId;
|
||||
}
|
||||
|
||||
// We found the correct txId: we update the event and delete the entry of the pending events.
|
||||
if (matchingTxId) {
|
||||
event.setTxnId(matchingTxId);
|
||||
event.setUnsigned({ ...event.getUnsigned(), transaction_id: matchingTxId });
|
||||
}
|
||||
this.pendingSendingEventsTxId = this.pendingSendingEventsTxId.filter((p) => p.id !== event.getId());
|
||||
|
||||
// Emit once there are no pending events anymore to release all other events that got
|
||||
// awaited in the `while (!matchingTxId && this.pendingSendingEventsTxId.length > 0)` loop
|
||||
// but are not send by this client.
|
||||
if (this.pendingSendingEventsTxId.length === 0) {
|
||||
this.eventEmitter.emit(RoomWidgetClientEvent.PendingEventsChanged);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onEvent = async (ev: CustomEvent<ISendEventToWidgetActionRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
|
||||
@@ -502,7 +570,37 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
// send us events from other rooms if this widget is always on screen
|
||||
if (ev.detail.data.room_id === this.roomId) {
|
||||
const event = new MatrixEvent(ev.detail.data as Partial<IEvent>);
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
|
||||
|
||||
// Only inject once we have update the txId
|
||||
await this.updateTxId(event);
|
||||
|
||||
// The widget API does not tell us whether a state event came from `state_after` or not so we assume legacy behaviour for now.
|
||||
if (this.syncApi instanceof SyncApi) {
|
||||
// The code will want to be something like:
|
||||
// ```
|
||||
// if (!params.addToTimeline && !params.addToState) {
|
||||
// // Passing undefined for `stateAfterEventList` makes `injectRoomEvents` run in "legacy mode"
|
||||
// // -> state events part of the `timelineEventList` parameter will update the state.
|
||||
// this.injectRoomEvents(this.room!, [], undefined, [event]);
|
||||
// } else {
|
||||
// this.injectRoomEvents(this.room!, undefined, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
|
||||
// }
|
||||
// ```
|
||||
|
||||
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
|
||||
// -> state events in `timelineEventList` will update the state.
|
||||
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
|
||||
} else {
|
||||
// The code will want to be something like:
|
||||
// ```
|
||||
// if (!params.addToTimeline && !params.addToState) {
|
||||
// this.injectRoomEvents(this.room!, [], [event]);
|
||||
// } else {
|
||||
// this.injectRoomEvents(this.room!, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
|
||||
// }
|
||||
// ```
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], [event]); // Sliding Sync
|
||||
}
|
||||
this.emit(ClientEvent.Event, event);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
||||
|
||||
@@ -878,7 +878,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
}
|
||||
}
|
||||
|
||||
if (this.manageMediaKeys && this.isJoined() && this.makeNewKeyTimeout === undefined) {
|
||||
if (this.manageMediaKeys && this.isJoined()) {
|
||||
const oldMembershipIds = new Set(
|
||||
oldMemberships.filter((m) => !this.isMyMembership(m)).map(getParticipantIdFromMembership),
|
||||
);
|
||||
@@ -896,8 +896,12 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
this.storeLastMembershipFingerprints();
|
||||
|
||||
if (anyLeft) {
|
||||
logger.debug(`Member(s) have left: queueing sender key rotation`);
|
||||
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, this.makeKeyDelay);
|
||||
if (this.makeNewKeyTimeout) {
|
||||
// existing rotation in progress, so let it complete
|
||||
} else {
|
||||
logger.debug(`Member(s) have left: queueing sender key rotation`);
|
||||
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, this.makeKeyDelay);
|
||||
}
|
||||
} else if (anyJoined) {
|
||||
logger.debug(`New member(s) have joined: re-sending keys`);
|
||||
this.requestSendCurrentKey();
|
||||
|
||||
@@ -56,7 +56,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
|
||||
|
||||
public start(): void {
|
||||
// We shouldn't need to null-check here, but matrix-client.spec.ts mocks getRooms
|
||||
// returing nothing, and breaks tests if you change it to return an empty array :'(
|
||||
// returning nothing, and breaks tests if you change it to return an empty array :'(
|
||||
for (const room of this.client.getRooms() ?? []) {
|
||||
const session = MatrixRTCSession.roomSessionForRoom(this.client, room);
|
||||
if (session.memberships.length > 0) {
|
||||
|
||||
@@ -58,13 +58,13 @@ export interface IRoomTimelineData {
|
||||
}
|
||||
|
||||
export interface IAddEventToTimelineOptions
|
||||
extends Pick<IAddEventOptions, "toStartOfTimeline" | "roomState" | "timelineWasEmpty"> {
|
||||
extends Pick<IAddEventOptions, "toStartOfTimeline" | "roomState" | "timelineWasEmpty" | "addToState"> {
|
||||
/** Whether the sync response came from cache */
|
||||
fromCache?: boolean;
|
||||
}
|
||||
|
||||
export interface IAddLiveEventOptions
|
||||
extends Pick<IAddEventToTimelineOptions, "fromCache" | "roomState" | "timelineWasEmpty"> {
|
||||
extends Pick<IAddEventToTimelineOptions, "fromCache" | "roomState" | "timelineWasEmpty" | "addToState"> {
|
||||
/** Applies to events in the timeline only. If this is 'replace' then if a
|
||||
* duplicate is encountered, the event passed to this function will replace
|
||||
* the existing event in the timeline. If this is not specified, or is
|
||||
@@ -391,6 +391,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
public addEventsToTimeline(
|
||||
events: MatrixEvent[],
|
||||
toStartOfTimeline: boolean,
|
||||
addToState: boolean,
|
||||
timeline: EventTimeline,
|
||||
paginationToken?: string | null,
|
||||
): void {
|
||||
@@ -495,6 +496,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
// we don't know about this event yet. Just add it to the timeline.
|
||||
this.addEventToTimeline(event, timeline, {
|
||||
toStartOfTimeline,
|
||||
addToState,
|
||||
});
|
||||
lastEventWasNew = true;
|
||||
didUpdate = true;
|
||||
@@ -592,7 +594,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
*/
|
||||
public addLiveEvent(
|
||||
event: MatrixEvent,
|
||||
{ duplicateStrategy, fromCache, roomState, timelineWasEmpty }: IAddLiveEventOptions = {},
|
||||
{ duplicateStrategy, fromCache, roomState, timelineWasEmpty, addToState }: IAddLiveEventOptions,
|
||||
): void {
|
||||
if (this.filter) {
|
||||
const events = this.filter.filterRoomTimeline([event]);
|
||||
@@ -630,6 +632,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
fromCache,
|
||||
roomState,
|
||||
timelineWasEmpty,
|
||||
addToState,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -649,40 +652,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
public addEventToTimeline(
|
||||
event: MatrixEvent,
|
||||
timeline: EventTimeline,
|
||||
{ toStartOfTimeline, fromCache, roomState, timelineWasEmpty }: IAddEventToTimelineOptions,
|
||||
): void;
|
||||
/**
|
||||
* @deprecated In favor of the overload with `IAddEventToTimelineOptions`
|
||||
*/
|
||||
public addEventToTimeline(
|
||||
event: MatrixEvent,
|
||||
timeline: EventTimeline,
|
||||
toStartOfTimeline: boolean,
|
||||
fromCache?: boolean,
|
||||
roomState?: RoomState,
|
||||
): void;
|
||||
public addEventToTimeline(
|
||||
event: MatrixEvent,
|
||||
timeline: EventTimeline,
|
||||
toStartOfTimelineOrOpts: boolean | IAddEventToTimelineOptions,
|
||||
fromCache = false,
|
||||
roomState?: RoomState,
|
||||
{ toStartOfTimeline, fromCache = false, roomState, timelineWasEmpty, addToState }: IAddEventToTimelineOptions,
|
||||
): void {
|
||||
let toStartOfTimeline = !!toStartOfTimelineOrOpts;
|
||||
let timelineWasEmpty: boolean | undefined;
|
||||
if (typeof toStartOfTimelineOrOpts === "object") {
|
||||
({ toStartOfTimeline, fromCache = false, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts);
|
||||
} else if (toStartOfTimelineOrOpts !== undefined) {
|
||||
// Deprecation warning
|
||||
// FIXME: Remove after 2023-06-01 (technical debt)
|
||||
logger.warn(
|
||||
"Overload deprecated: " +
|
||||
"`EventTimelineSet.addEventToTimeline(event, timeline, toStartOfTimeline, fromCache?, roomState?)` " +
|
||||
"is deprecated in favor of the overload with " +
|
||||
"`EventTimelineSet.addEventToTimeline(event, timeline, IAddEventToTimelineOptions)`",
|
||||
);
|
||||
}
|
||||
|
||||
if (timeline.getTimelineSet() !== this) {
|
||||
throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
|
||||
"in timelineSet(threadId=${this.thread?.id})`);
|
||||
@@ -713,6 +684,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
toStartOfTimeline,
|
||||
roomState,
|
||||
timelineWasEmpty,
|
||||
addToState,
|
||||
});
|
||||
this._eventIdToTimeline.set(eventId, timeline);
|
||||
|
||||
@@ -741,7 +713,12 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* @remarks
|
||||
* Fires {@link RoomEvent.Timeline}
|
||||
*/
|
||||
public insertEventIntoTimeline(event: MatrixEvent, timeline: EventTimeline, roomState: RoomState): void {
|
||||
public insertEventIntoTimeline(
|
||||
event: MatrixEvent,
|
||||
timeline: EventTimeline,
|
||||
roomState: RoomState,
|
||||
addToState: boolean,
|
||||
): void {
|
||||
if (timeline.getTimelineSet() !== this) {
|
||||
throw new Error(`EventTimelineSet.insertEventIntoTimeline: Timeline=${timeline.toString()} does not belong " +
|
||||
"in timelineSet(threadId=${this.thread?.id})`);
|
||||
@@ -777,6 +754,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
fromCache: false,
|
||||
timelineWasEmpty: false,
|
||||
roomState,
|
||||
addToState,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -799,7 +777,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
// If we got to the end of the loop, insertIndex points at the end of
|
||||
// the list.
|
||||
|
||||
timeline.insertEvent(event, insertIndex, roomState);
|
||||
timeline.insertEvent(event, insertIndex, roomState, addToState);
|
||||
this._eventIdToTimeline.set(eventId, timeline);
|
||||
|
||||
const data: IRoomTimelineData = {
|
||||
@@ -832,6 +810,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
} else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
|
||||
this.addEventToTimeline(localEvent, this.liveTimeline, {
|
||||
toStartOfTimeline: false,
|
||||
addToState: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ export interface IAddEventOptions extends Pick<IMarkerFoundOptions, "timelineWas
|
||||
toStartOfTimeline: boolean;
|
||||
/** The state events to reconcile metadata from */
|
||||
roomState?: RoomState;
|
||||
/** Whether to add timeline events to the state as was done in legacy sync v2.
|
||||
* If true then timeline events will be added to the state.
|
||||
* In sync v2 with org.matrix.msc4222.use_state_after and simplified sliding sync,
|
||||
* all state arrives explicitly and timeline events should not be added. */
|
||||
addToState: boolean;
|
||||
}
|
||||
|
||||
export enum Direction {
|
||||
@@ -127,7 +132,7 @@ export class EventTimeline {
|
||||
public constructor(private readonly eventTimelineSet: EventTimelineSet) {
|
||||
this.roomId = eventTimelineSet.room?.roomId ?? null;
|
||||
if (this.roomId) {
|
||||
this.startState = new RoomState(this.roomId, undefined, true);
|
||||
this.startState = new RoomState(this.roomId);
|
||||
this.endState = new RoomState(this.roomId);
|
||||
}
|
||||
|
||||
@@ -362,7 +367,7 @@ export class EventTimeline {
|
||||
*/
|
||||
public addEvent(
|
||||
event: MatrixEvent,
|
||||
{ toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions = { toStartOfTimeline: false },
|
||||
{ toStartOfTimeline, roomState, timelineWasEmpty, addToState }: IAddEventOptions,
|
||||
): void {
|
||||
if (!roomState) {
|
||||
roomState = toStartOfTimeline ? this.startState : this.endState;
|
||||
@@ -374,7 +379,7 @@ export class EventTimeline {
|
||||
EventTimeline.setEventMetadata(event, roomState!, toStartOfTimeline);
|
||||
|
||||
// modify state but only on unfiltered timelineSets
|
||||
if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
|
||||
if (addToState && event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
|
||||
roomState?.setStateEvents([event], { timelineWasEmpty });
|
||||
// it is possible that the act of setting the state event means we
|
||||
// can set more metadata (specifically sender/target props), so try
|
||||
@@ -417,14 +422,14 @@ export class EventTimeline {
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public insertEvent(event: MatrixEvent, insertIndex: number, roomState: RoomState): void {
|
||||
public insertEvent(event: MatrixEvent, insertIndex: number, roomState: RoomState, addToState: boolean): void {
|
||||
const timelineSet = this.getTimelineSet();
|
||||
|
||||
if (timelineSet.room) {
|
||||
EventTimeline.setEventMetadata(event, roomState, false);
|
||||
|
||||
// modify state but only on unfiltered timelineSets
|
||||
if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
|
||||
if (addToState && event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
|
||||
roomState.setStateEvents([event], {});
|
||||
// it is possible that the act of setting the state event means we
|
||||
// can set more metadata (specifically sender/target props), so try
|
||||
|
||||
+8
-6
@@ -559,9 +559,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
return {} as T;
|
||||
}
|
||||
if (this.clearEvent) {
|
||||
return (this.clearEvent.content || {}) as T;
|
||||
return (this.clearEvent.content ?? {}) as T;
|
||||
}
|
||||
return (this.event.content || {}) as T;
|
||||
return (this.event.content ?? {}) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -575,7 +575,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
if (this._localRedactionEvent) {
|
||||
return {} as T;
|
||||
} else if (this._replacingEvent) {
|
||||
return this._replacingEvent.getContent()["m.new_content"] || {};
|
||||
return this._replacingEvent.getContent()["m.new_content"] ?? {};
|
||||
} else {
|
||||
return this.getOriginalContent();
|
||||
}
|
||||
@@ -1250,7 +1250,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
const timeline = room.getLiveTimeline();
|
||||
// We use insertEventIntoTimeline to insert it in timestamp order,
|
||||
// because we don't know where it should go (until we have MSC4033).
|
||||
timeline.getTimelineSet().insertEventIntoTimeline(this, timeline, timeline.getState(EventTimeline.FORWARDS)!);
|
||||
timeline
|
||||
.getTimelineSet()
|
||||
.insertEventIntoTimeline(this, timeline, timeline.getState(EventTimeline.FORWARDS)!, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1394,7 +1396,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
|
||||
}
|
||||
|
||||
this.localTimestamp = Date.now() - this.getAge()!;
|
||||
this.localTimestamp = Date.now() - (this.getAge() ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1631,7 +1633,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* @param otherEvent - The other event to check against.
|
||||
* @returns True if the events are the same, false otherwise.
|
||||
*/
|
||||
public isEquivalentTo(otherEvent: MatrixEvent): boolean {
|
||||
public isEquivalentTo(otherEvent?: MatrixEvent): boolean {
|
||||
if (!otherEvent) return false;
|
||||
if (otherEvent === this) return true;
|
||||
const myProps = deepSortedObjectEntries(this.event);
|
||||
|
||||
@@ -20,6 +20,7 @@ import { RelationType } from "../@types/event.ts";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter.ts";
|
||||
import { MatrixClient } from "../client.ts";
|
||||
import { Room } from "./room.ts";
|
||||
import { CryptoBackend } from "../common-crypto/CryptoBackend.ts";
|
||||
|
||||
export enum RelationsEvent {
|
||||
Add = "Relations.add",
|
||||
@@ -323,8 +324,9 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
||||
return event;
|
||||
}, null);
|
||||
|
||||
if (lastReplacement?.shouldAttemptDecryption() && this.client.isCryptoEnabled()) {
|
||||
await lastReplacement.attemptDecryption(this.client.crypto!);
|
||||
if (lastReplacement?.shouldAttemptDecryption() && this.client.getCrypto()) {
|
||||
// Dirty but we are expecting to pass the cryptoBackend which is not accessible here
|
||||
await lastReplacement.attemptDecryption(this.client.getCrypto() as CryptoBackend);
|
||||
} else if (lastReplacement?.isBeingDecrypted()) {
|
||||
await lastReplacement.getDecryptionPromise();
|
||||
}
|
||||
|
||||
@@ -408,17 +408,18 @@ const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
|
||||
|
||||
function shouldDisambiguate(selfUserId: string, displayName?: string, roomState?: RoomState): boolean {
|
||||
if (!displayName || displayName === selfUserId) return false;
|
||||
if (!roomState) return false;
|
||||
|
||||
const strippedDisplayName = removeHiddenChars(displayName);
|
||||
|
||||
// First check if the displayname is something we consider truthy
|
||||
// after stripping it of zero width characters and padding spaces
|
||||
if (!removeHiddenChars(displayName)) return false;
|
||||
|
||||
if (!roomState) return false;
|
||||
if (!strippedDisplayName) return false;
|
||||
|
||||
// Next check if the name contains something that look like a mxid
|
||||
// If it does, it may be someone trying to impersonate someone else
|
||||
// Show full mxid in this case
|
||||
if (MXID_PATTERN.test(displayName)) return true;
|
||||
if (MXID_PATTERN.test(strippedDisplayName)) return true;
|
||||
|
||||
// Also show mxid if the display name contains any LTR/RTL characters as these
|
||||
// make it very difficult for us to find similar *looking* display names
|
||||
|
||||
@@ -194,22 +194,10 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
* As the timeline might get reset while they are loading, this state needs to be inherited
|
||||
* and shared when the room state is cloned for the new timeline.
|
||||
* This should only be passed from clone.
|
||||
* @param isStartTimelineState - Optional. This state is marked as a start state.
|
||||
* This is used to skip state insertions that are
|
||||
* in the wrong order. The order is determined by the `replaces_state` id.
|
||||
*
|
||||
* Example:
|
||||
* A current state events `replaces_state` value is `1`.
|
||||
* Trying to insert a state event with `event_id` `1` in its place would fail if isStartTimelineState = false.
|
||||
*
|
||||
* A current state events `event_id` is `2`.
|
||||
* Trying to insert a state event where its `replaces_state` value is `2` would fail if isStartTimelineState = true.
|
||||
*/
|
||||
|
||||
public constructor(
|
||||
public readonly roomId: string,
|
||||
private oobMemberFlags = { status: OobStatus.NotStarted },
|
||||
public readonly isStartTimelineState = false,
|
||||
) {
|
||||
super();
|
||||
this.updateModifiedTime();
|
||||
@@ -420,7 +408,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
* Fires {@link RoomStateEvent.Events}
|
||||
* Fires {@link RoomStateEvent.Marker}
|
||||
*/
|
||||
public setStateEvents(stateEvents: MatrixEvent[], options?: IMarkerFoundOptions): void {
|
||||
public setStateEvents(stateEvents: MatrixEvent[], markerFoundOptions?: IMarkerFoundOptions): void {
|
||||
this.updateModifiedTime();
|
||||
|
||||
// update the core event dict
|
||||
@@ -432,22 +420,6 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
}
|
||||
|
||||
const lastStateEvent = this.getStateEventMatching(event);
|
||||
|
||||
// Safety measure to not update the room (and emit the update) with older state.
|
||||
// The sync loop really should not send old events but it does very regularly.
|
||||
// Logging on return in those two conditions results in a large amount of logging. (on startup and when running element)
|
||||
const lastReplaceId = lastStateEvent?.event.unsigned?.replaces_state;
|
||||
const lastId = lastStateEvent?.event.event_id;
|
||||
const newReplaceId = event.event.unsigned?.replaces_state;
|
||||
const newId = event.event.event_id;
|
||||
if (this.isStartTimelineState) {
|
||||
// Add an event to the start of the timeline. Its replace id should not be the same as the one of the current/last start state event.
|
||||
if (newReplaceId && lastId && newReplaceId === lastId) return;
|
||||
} else {
|
||||
// Add an event to the end of the timeline. It should not be the same as the one replaced by the current/last end state event.
|
||||
if (lastReplaceId && newId && lastReplaceId === newId) return;
|
||||
}
|
||||
|
||||
this.setStateEvent(event);
|
||||
if (event.getType() === EventType.RoomMember) {
|
||||
this.updateDisplayNameCache(event.getStateKey()!, event.getContent().displayname ?? "");
|
||||
@@ -504,7 +476,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
// assume all our sentinels are now out-of-date
|
||||
this.sentinels = {};
|
||||
} else if (UNSTABLE_MSC2716_MARKER.matches(event.getType())) {
|
||||
this.emit(RoomStateEvent.Marker, event, options);
|
||||
this.emit(RoomStateEvent.Marker, event, markerFoundOptions);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+20
-8
@@ -545,7 +545,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @returns Signals when all events have been decrypted
|
||||
*/
|
||||
public async decryptCriticalEvents(): Promise<void> {
|
||||
if (!this.client.isCryptoEnabled()) return;
|
||||
if (!this.client.getCrypto()) return;
|
||||
|
||||
const readReceiptEventId = this.getEventReadUpTo(this.client.getUserId()!, true);
|
||||
const events = this.getLiveTimeline().getEvents();
|
||||
@@ -567,7 +567,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @returns Signals when all events have been decrypted
|
||||
*/
|
||||
public async decryptAllEvents(): Promise<void> {
|
||||
if (!this.client.isCryptoEnabled()) return;
|
||||
if (!this.client.getCrypto()) return;
|
||||
|
||||
const decryptionPromises = this.getUnfilteredTimelineSet()
|
||||
.getLiveTimeline()
|
||||
@@ -1739,10 +1739,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
public addEventsToTimeline(
|
||||
events: MatrixEvent[],
|
||||
toStartOfTimeline: boolean,
|
||||
addToState: boolean,
|
||||
timeline: EventTimeline,
|
||||
paginationToken?: string,
|
||||
): void {
|
||||
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken);
|
||||
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, addToState, timeline, paginationToken);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1907,7 +1908,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
// see https://github.com/vector-im/vector-web/issues/2109
|
||||
|
||||
unfilteredLiveTimeline.getEvents().forEach(function (event) {
|
||||
timelineSet.addLiveEvent(event);
|
||||
timelineSet.addLiveEvent(event, { addToState: false }); // Filtered timeline sets should not track state
|
||||
});
|
||||
|
||||
// find the earliest unfiltered timeline
|
||||
@@ -1994,6 +1995,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
if (filterType !== ThreadFilterType.My || currentUserParticipated) {
|
||||
timelineSet.getLiveTimeline().addEvent(thread.rootEvent!, {
|
||||
toStartOfTimeline: false,
|
||||
addToState: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -2068,6 +2070,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
const opts = {
|
||||
duplicateStrategy: DuplicateStrategy.Ignore,
|
||||
fromCache: false,
|
||||
addToState: false,
|
||||
roomState,
|
||||
};
|
||||
this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts);
|
||||
@@ -2190,6 +2193,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
duplicateStrategy: DuplicateStrategy.Replace,
|
||||
fromCache: false,
|
||||
roomState,
|
||||
addToState: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2381,9 +2385,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
duplicateStrategy: DuplicateStrategy.Replace,
|
||||
fromCache: false,
|
||||
roomState: this.currentState,
|
||||
addToState: false,
|
||||
});
|
||||
} else {
|
||||
timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), { toStartOfTimeline });
|
||||
timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), {
|
||||
toStartOfTimeline,
|
||||
addToState: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2540,7 +2548,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* Fires {@link RoomEvent.Timeline}
|
||||
*/
|
||||
private addLiveEvent(event: MatrixEvent, addLiveEventOptions: IAddLiveEventOptions): void {
|
||||
const { duplicateStrategy, timelineWasEmpty, fromCache } = addLiveEventOptions;
|
||||
const { duplicateStrategy, timelineWasEmpty, fromCache, addToState } = addLiveEventOptions;
|
||||
|
||||
// add to our timeline sets
|
||||
for (const timelineSet of this.timelineSets) {
|
||||
@@ -2548,6 +2556,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
duplicateStrategy,
|
||||
fromCache,
|
||||
timelineWasEmpty,
|
||||
addToState,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2631,11 +2640,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
if (timelineSet.getFilter()!.filterRoomTimeline([event]).length) {
|
||||
timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
|
||||
toStartOfTimeline: false,
|
||||
addToState: false, // We don't support localEcho of state events yet
|
||||
});
|
||||
}
|
||||
} else {
|
||||
timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
|
||||
toStartOfTimeline: false,
|
||||
addToState: false, // We don't support localEcho of state events yet
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2886,8 +2897,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @param addLiveEventOptions - addLiveEvent options
|
||||
* @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'.
|
||||
*/
|
||||
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise<void> {
|
||||
const { duplicateStrategy, fromCache, timelineWasEmpty = false } = addLiveEventOptions ?? {};
|
||||
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions: IAddLiveEventOptions): Promise<void> {
|
||||
const { duplicateStrategy, fromCache, timelineWasEmpty = false, addToState } = addLiveEventOptions;
|
||||
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
||||
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
||||
}
|
||||
@@ -2902,6 +2913,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
duplicateStrategy,
|
||||
fromCache,
|
||||
timelineWasEmpty,
|
||||
addToState,
|
||||
};
|
||||
|
||||
// List of extra events to check for being parents of any relations encountered
|
||||
|
||||
@@ -208,6 +208,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
|
||||
public static setServerSideSupport(status: FeatureSupport): void {
|
||||
Thread.hasServerSideSupport = status;
|
||||
// XXX: This global latching behaviour is really unexpected and means that you can't undo when moving to a server without support
|
||||
if (status !== FeatureSupport.Stable) {
|
||||
FILTER_RELATED_BY_SENDERS.setPreferUnstable(true);
|
||||
FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true);
|
||||
@@ -317,6 +318,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
toStartOfTimeline,
|
||||
fromCache: false,
|
||||
roomState: this.roomState,
|
||||
addToState: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -343,7 +345,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
if (this.findEventById(eventId)) {
|
||||
return;
|
||||
}
|
||||
this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState);
|
||||
this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState, false);
|
||||
}
|
||||
|
||||
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
@@ -618,7 +620,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
// if the thread has regular events, this will just load the last reply.
|
||||
// if the thread is newly created, this will load the root event.
|
||||
if (this.replyCount === 0 && this.rootEvent) {
|
||||
this.timelineSet.addEventsToTimeline([this.rootEvent], true, this.liveTimeline, null);
|
||||
this.timelineSet.addEventsToTimeline([this.rootEvent], true, false, this.liveTimeline, null);
|
||||
this.liveTimeline.setPaginationToken(null, Direction.Backward);
|
||||
} else {
|
||||
this.initalEventFetchProm = this.client.paginateEventTimeline(this.liveTimeline, {
|
||||
|
||||
+202
-11
@@ -24,11 +24,12 @@ import {
|
||||
KeyBackupInfo,
|
||||
KeyBackupSession,
|
||||
Curve25519SessionData,
|
||||
KeyBackupRestoreOpts,
|
||||
KeyBackupRestoreResult,
|
||||
KeyBackupRoomSessions,
|
||||
} from "../crypto-api/keybackup.ts";
|
||||
import { logger } from "../logger.ts";
|
||||
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts";
|
||||
import { IMegolmSessionData } from "../crypto/index.ts";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
import { encodeUri, logDuration } from "../utils.ts";
|
||||
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts";
|
||||
@@ -36,6 +37,7 @@ import { sleep } from "../utils.ts";
|
||||
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
|
||||
import { ImportRoomKeyProgressData, ImportRoomKeysOpts, CryptoEvent } from "../crypto-api/index.ts";
|
||||
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||
import { IMegolmSessionData } from "../@types/crypto.ts";
|
||||
|
||||
/** Authentification of the backup info, depends on algorithm */
|
||||
type AuthData = KeyBackupInfo["auth_data"];
|
||||
@@ -218,7 +220,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
/**
|
||||
* Import a list of room keys previously exported by exportRoomKeysAsJson
|
||||
*
|
||||
* @param keys - a JSON string encoding a list of session export objects,
|
||||
* @param jsonKeys - a JSON string encoding a list of session export objects,
|
||||
* each of which is an IMegolmSessionData
|
||||
* @param opts - options object
|
||||
* @returns a promise which resolves once the keys have been imported
|
||||
@@ -495,20 +497,19 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
*/
|
||||
private keysCountInBatch(batch: RustSdkCryptoJs.KeysBackupRequest): number {
|
||||
const parsedBody: KeyBackup = JSON.parse(batch.body);
|
||||
let count = 0;
|
||||
for (const { sessions } of Object.values(parsedBody.rooms)) {
|
||||
count += Object.keys(sessions).length;
|
||||
}
|
||||
return count;
|
||||
return countKeysInBackup(parsedBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the current key backup from the server
|
||||
* Get information about a key backup from the server
|
||||
* - If version is provided, get information about that backup version.
|
||||
* - If no version is provided, get information about the latest backup.
|
||||
*
|
||||
* @param version - The version of the backup to get information about.
|
||||
* @returns Information object from API or null if there is no active backup.
|
||||
*/
|
||||
private async requestKeyBackupVersion(): Promise<KeyBackupInfo | null> {
|
||||
return await requestKeyBackupVersion(this.http);
|
||||
public async requestKeyBackupVersion(version?: string): Promise<KeyBackupInfo | null> {
|
||||
return await requestKeyBackupVersion(this.http, version);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -582,6 +583,11 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
await this.http.authedRequest<void>(Method.Delete, path, undefined, undefined, {
|
||||
prefix: ClientPrefix.V3,
|
||||
});
|
||||
// If the backup we are deleting is the active one, we need to disable the key backup and to have the local properties reset
|
||||
if (this.activeBackupVersion === version) {
|
||||
this.serverBackupInfo = null;
|
||||
await this.disableKeyBackup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -591,6 +597,148 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
public createBackupDecryptor(decryptionKey: RustSdkCryptoJs.BackupDecryptionKey): BackupDecryptor {
|
||||
return new RustBackupDecryptor(decryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a key backup.
|
||||
*
|
||||
* @param backupVersion - The version of the backup to restore.
|
||||
* @param backupDecryptor - The backup decryptor to use to decrypt the keys.
|
||||
* @param opts - Options for the restore.
|
||||
* @returns The total number of keys and the total imported.
|
||||
*/
|
||||
public async restoreKeyBackup(
|
||||
backupVersion: string,
|
||||
backupDecryptor: BackupDecryptor,
|
||||
opts?: KeyBackupRestoreOpts,
|
||||
): Promise<KeyBackupRestoreResult> {
|
||||
const keyBackup = await this.downloadKeyBackup(backupVersion);
|
||||
opts?.progressCallback?.({
|
||||
stage: "load_keys",
|
||||
});
|
||||
|
||||
return this.importKeyBackup(keyBackup, backupVersion, backupDecryptor, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `/room_keys/keys` to download the key backup (room keys) for the given backup version.
|
||||
* https://spec.matrix.org/v1.12/client-server-api/#get_matrixclientv3room_keyskeys
|
||||
*
|
||||
* @param backupVersion
|
||||
* @returns The key backup response.
|
||||
*/
|
||||
private downloadKeyBackup(backupVersion: string): Promise<KeyBackup> {
|
||||
return this.http.authedRequest<KeyBackup>(
|
||||
Method.Get,
|
||||
"/room_keys/keys",
|
||||
{ version: backupVersion },
|
||||
undefined,
|
||||
{
|
||||
prefix: ClientPrefix.V3,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the room keys from a `/room_keys/keys` call.
|
||||
* Calls `opts.progressCallback` with the progress of the import.
|
||||
*
|
||||
* @param keyBackup - The response from the server containing the keys to import.
|
||||
* @param backupVersion - The version of the backup info.
|
||||
* @param backupDecryptor - The backup decryptor to use to decrypt the keys.
|
||||
* @param opts - Options for the import.
|
||||
*
|
||||
* @returns The total number of keys and the total imported.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private async importKeyBackup(
|
||||
keyBackup: KeyBackup,
|
||||
backupVersion: string,
|
||||
backupDecryptor: BackupDecryptor,
|
||||
opts?: KeyBackupRestoreOpts,
|
||||
): Promise<KeyBackupRestoreResult> {
|
||||
// We have a full backup here, it can get quite big, so we need to decrypt and import it in chunks.
|
||||
|
||||
const CHUNK_SIZE = 200;
|
||||
// Get the total count as a first pass
|
||||
const totalKeyCount = countKeysInBackup(keyBackup);
|
||||
let totalImported = 0;
|
||||
let totalFailures = 0;
|
||||
|
||||
/**
|
||||
* This method is called when we have enough chunks to decrypt.
|
||||
* It will decrypt the chunks and try to import the room keys.
|
||||
* @param roomChunks
|
||||
*/
|
||||
const handleChunkCallback = async (roomChunks: Map<string, KeyBackupRoomSessions>): Promise<void> => {
|
||||
const currentChunk: IMegolmSessionData[] = [];
|
||||
for (const roomId of roomChunks.keys()) {
|
||||
// Decrypt the sessions for the given room
|
||||
const decryptedSessions = await backupDecryptor.decryptSessions(roomChunks.get(roomId)!);
|
||||
// Add the decrypted sessions to the current chunk
|
||||
decryptedSessions.forEach((session) => {
|
||||
// We set the room_id for each session
|
||||
session.room_id = roomId;
|
||||
currentChunk.push(session);
|
||||
});
|
||||
}
|
||||
|
||||
// We have a chunk of decrypted keys: import them
|
||||
try {
|
||||
await this.importBackedUpRoomKeys(currentChunk, backupVersion);
|
||||
totalImported += currentChunk.length;
|
||||
} catch (e) {
|
||||
totalFailures += currentChunk.length;
|
||||
// We failed to import some keys, but we should still try to import the rest?
|
||||
// Log the error and continue
|
||||
logger.error("Error importing keys from backup", e);
|
||||
}
|
||||
|
||||
opts?.progressCallback?.({
|
||||
total: totalKeyCount,
|
||||
successes: totalImported,
|
||||
stage: "load_keys",
|
||||
failures: totalFailures,
|
||||
});
|
||||
};
|
||||
|
||||
let groupChunkCount = 0;
|
||||
let chunkGroupByRoom: Map<string, KeyBackupRoomSessions> = new Map();
|
||||
|
||||
// Iterate over the rooms and sessions to group them in chunks
|
||||
// And we call the handleChunkCallback when we have enough chunks to decrypt
|
||||
for (const [roomId, roomData] of Object.entries(keyBackup.rooms)) {
|
||||
// If there are no sessions for the room, skip it
|
||||
if (!roomData.sessions) continue;
|
||||
|
||||
// Initialize a new chunk group for the current room
|
||||
chunkGroupByRoom.set(roomId, {});
|
||||
|
||||
for (const [sessionId, session] of Object.entries(roomData.sessions)) {
|
||||
// We set previously the chunk group for the current room, so we can safely get it
|
||||
const sessionsForRoom = chunkGroupByRoom.get(roomId)!;
|
||||
sessionsForRoom[sessionId] = session;
|
||||
groupChunkCount += 1;
|
||||
// If we have enough chunks to decrypt, call the block callback
|
||||
if (groupChunkCount >= CHUNK_SIZE) {
|
||||
// We have enough chunks to decrypt
|
||||
await handleChunkCallback(chunkGroupByRoom);
|
||||
// Reset the chunk group
|
||||
chunkGroupByRoom = new Map();
|
||||
// There might be remaining keys for that room, so add back an entry for the current room.
|
||||
chunkGroupByRoom.set(roomId, {});
|
||||
groupChunkCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle remaining chunk if needed
|
||||
if (groupChunkCount > 0) {
|
||||
await handleChunkCallback(chunkGroupByRoom);
|
||||
}
|
||||
|
||||
return { total: totalKeyCount, imported: totalImported };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -657,11 +805,26 @@ export class RustBackupDecryptor implements BackupDecryptor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a key backup info from the server.
|
||||
*
|
||||
* If `version` is provided, calls `GET /room_keys/version/$version` and gets the backup info for that version.
|
||||
* See https://spec.matrix.org/v1.12/client-server-api/#get_matrixclientv3room_keysversionversion.
|
||||
*
|
||||
* If not, calls `GET /room_keys/version` and gets the latest backup info.
|
||||
* See https://spec.matrix.org/v1.12/client-server-api/#get_matrixclientv3room_keysversion
|
||||
*
|
||||
* @param http
|
||||
* @param version - the specific version of the backup info to fetch
|
||||
* @returns The key backup info or null if there is no backup.
|
||||
*/
|
||||
export async function requestKeyBackupVersion(
|
||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
version?: string,
|
||||
): Promise<KeyBackupInfo | null> {
|
||||
try {
|
||||
return await http.authedRequest<KeyBackupInfo>(Method.Get, "/room_keys/version", undefined, undefined, {
|
||||
const path = version ? encodeUri("/room_keys/version/$version", { $version: version }) : "/room_keys/version";
|
||||
return await http.authedRequest<KeyBackupInfo>(Method.Get, path, undefined, undefined, {
|
||||
prefix: ClientPrefix.V3,
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -673,6 +836,34 @@ export async function requestKeyBackupVersion(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided decryption key matches the public key of the key backup info.
|
||||
*
|
||||
* @param decryptionKey - The decryption key to check.
|
||||
* @param keyBackupInfo - The key backup info to check against.
|
||||
* @returns `true` if the decryption key matches the key backup info, `false` otherwise.
|
||||
*/
|
||||
export function decryptionKeyMatchesKeyBackupInfo(
|
||||
decryptionKey: RustSdkCryptoJs.BackupDecryptionKey,
|
||||
keyBackupInfo: KeyBackupInfo,
|
||||
): boolean {
|
||||
const authData = <Curve25519AuthData>keyBackupInfo.auth_data;
|
||||
return authData.public_key === decryptionKey.megolmV1PublicKey.publicKeyBase64;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the total number of keys present in a key backup.
|
||||
* @param keyBackup - The key backup to count the keys from.
|
||||
* @returns The total number of keys in the backup.
|
||||
*/
|
||||
function countKeysInBackup(keyBackup: KeyBackup): number {
|
||||
let count = 0;
|
||||
for (const { sessions } of Object.values(keyBackup.rooms)) {
|
||||
count += Object.keys(sessions).length;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export type RustBackupCryptoEvents =
|
||||
| CryptoEvent.KeyBackupStatus
|
||||
| CryptoEvent.KeyBackupSessionsRemaining
|
||||
|
||||
@@ -21,7 +21,6 @@ import { CryptoStore, MigrationState, SecretStorePrivateKeys } from "../crypto/s
|
||||
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
|
||||
import { IHttpOpts, MatrixHttpApi } from "../http-api/index.ts";
|
||||
import { requestKeyBackupVersion } from "./backup.ts";
|
||||
import { IRoomEncryption } from "../crypto/RoomList.ts";
|
||||
import { CrossSigningKeyInfo, Curve25519AuthData } from "../crypto-api/index.ts";
|
||||
import { RustCrypto } from "./rust-crypto.ts";
|
||||
import { KeyBackupInfo } from "../crypto-api/keybackup.ts";
|
||||
@@ -30,6 +29,12 @@ import { encodeBase64 } from "../base64.ts";
|
||||
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
|
||||
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||
|
||||
interface LegacyRoomEncryption {
|
||||
algorithm: string;
|
||||
rotation_period_ms?: number;
|
||||
rotation_period_msgs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if any data needs migrating from the legacy store, and do so.
|
||||
*
|
||||
@@ -375,7 +380,7 @@ export async function migrateRoomSettingsFromLegacyCrypto({
|
||||
return;
|
||||
}
|
||||
|
||||
let rooms: Record<string, IRoomEncryption> = {};
|
||||
let rooms: Record<string, LegacyRoomEncryption> = {};
|
||||
|
||||
await legacyStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
|
||||
legacyStore.getEndToEndRooms(txn, (result) => {
|
||||
|
||||
+114
-29
@@ -46,7 +46,6 @@ import {
|
||||
CrossSigningStatus,
|
||||
CryptoApi,
|
||||
CryptoCallbacks,
|
||||
Curve25519AuthData,
|
||||
DecryptionFailureCode,
|
||||
DeviceVerificationStatus,
|
||||
EventEncryptionInfo,
|
||||
@@ -66,6 +65,8 @@ import {
|
||||
DeviceIsolationModeKind,
|
||||
CryptoEvent,
|
||||
CryptoEventHandlerMap,
|
||||
KeyBackupRestoreOpts,
|
||||
KeyBackupRestoreResult,
|
||||
} from "../crypto-api/index.ts";
|
||||
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
|
||||
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
|
||||
@@ -76,16 +77,17 @@ import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys }
|
||||
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts";
|
||||
import { EventType, MsgType } from "../@types/event.ts";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
import { RustBackupManager } from "./backup.ts";
|
||||
import { decryptionKeyMatchesKeyBackupInfo, RustBackupManager } from "./backup.ts";
|
||||
import { TypedReEmitter } from "../ReEmitter.ts";
|
||||
import { randomString } from "../randomstring.ts";
|
||||
import { ClientStoppedError } from "../errors.ts";
|
||||
import { ISignatures } from "../@types/signed.ts";
|
||||
import { encodeBase64 } from "../base64.ts";
|
||||
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
||||
import { OutgoingRequestsManager } from "./OutgoingRequestsManager.ts";
|
||||
import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader.ts";
|
||||
import { DehydratedDeviceManager } from "./DehydratedDeviceManager.ts";
|
||||
import { VerificationMethod } from "../types.ts";
|
||||
import { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
|
||||
|
||||
const ALL_VERIFICATION_METHODS = [
|
||||
VerificationMethod.Sas,
|
||||
@@ -327,7 +329,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
|
||||
*/
|
||||
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
|
||||
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise<BackupDecryptor> {
|
||||
if (!(privKey instanceof Uint8Array)) {
|
||||
throw new Error(`getBackupDecryptor: expects Uint8Array`);
|
||||
}
|
||||
@@ -337,9 +339,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
}
|
||||
|
||||
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(privKey));
|
||||
|
||||
const authData = <Curve25519AuthData>backupInfo.auth_data;
|
||||
if (authData.public_key != backupDecryptionKey.megolmV1PublicKey.publicKeyBase64) {
|
||||
if (!decryptionKeyMatchesKeyBackupInfo(backupDecryptionKey, backupInfo)) {
|
||||
throw new Error(`getBackupDecryptor: key backup on server does not match the decryption key`);
|
||||
}
|
||||
|
||||
@@ -653,7 +653,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
* Implementation of {@link CryptoApi#getUserVerificationStatus}.
|
||||
*/
|
||||
public async getUserVerificationStatus(userId: string): Promise<UserVerificationStatus> {
|
||||
const userIdentity: RustSdkCryptoJs.UserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
|
||||
const userIdentity: RustSdkCryptoJs.OtherUserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
|
||||
await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(userId));
|
||||
if (userIdentity === undefined) {
|
||||
return new UserVerificationStatus(false, false, false);
|
||||
@@ -662,7 +662,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
const verified = userIdentity.isVerified();
|
||||
const wasVerified = userIdentity.wasPreviouslyVerified();
|
||||
const needsUserApproval =
|
||||
userIdentity instanceof RustSdkCryptoJs.UserIdentity ? userIdentity.identityNeedsUserApproval() : false;
|
||||
userIdentity instanceof RustSdkCryptoJs.OtherUserIdentity
|
||||
? userIdentity.identityNeedsUserApproval()
|
||||
: false;
|
||||
userIdentity.free();
|
||||
return new UserVerificationStatus(verified, wasVerified, false, needsUserApproval);
|
||||
}
|
||||
@@ -671,7 +673,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
* Implementation of {@link CryptoApi#pinCurrentUserIdentity}.
|
||||
*/
|
||||
public async pinCurrentUserIdentity(userId: string): Promise<void> {
|
||||
const userIdentity: RustSdkCryptoJs.UserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
|
||||
const userIdentity: RustSdkCryptoJs.OtherUserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
|
||||
await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(userId));
|
||||
|
||||
if (userIdentity === undefined) {
|
||||
@@ -1020,7 +1022,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
* Implementation of {@link CryptoApi#requestVerificationDM}
|
||||
*/
|
||||
public async requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
|
||||
const userIdentity: RustSdkCryptoJs.UserIdentity | undefined = await this.olmMachine.getIdentity(
|
||||
const userIdentity: RustSdkCryptoJs.OtherUserIdentity | undefined = await this.olmMachine.getIdentity(
|
||||
new RustSdkCryptoJs.UserId(userId),
|
||||
);
|
||||
|
||||
@@ -1178,7 +1180,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> {
|
||||
const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys();
|
||||
if (!backupKeys.decryptionKey) return null;
|
||||
return Buffer.from(backupKeys.decryptionKey.toBase64(), "base64");
|
||||
return decodeBase64(backupKeys.decryptionKey.toBase64());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1202,6 +1204,28 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#loadSessionBackupPrivateKeyFromSecretStorage}.
|
||||
*/
|
||||
public async loadSessionBackupPrivateKeyFromSecretStorage(): Promise<void> {
|
||||
const backupKey = await this.secretStorage.get("m.megolm_backup.v1");
|
||||
if (!backupKey) {
|
||||
throw new Error("loadSessionBackupPrivateKeyFromSecretStorage: missing decryption key in secret storage");
|
||||
}
|
||||
|
||||
const keyBackupInfo = await this.backupManager.getServerBackupInfo();
|
||||
if (!keyBackupInfo || !keyBackupInfo.version) {
|
||||
throw new Error("loadSessionBackupPrivateKeyFromSecretStorage: unable to get backup version");
|
||||
}
|
||||
|
||||
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(backupKey);
|
||||
if (!decryptionKeyMatchesKeyBackupInfo(backupDecryptionKey, keyBackupInfo)) {
|
||||
throw new Error("loadSessionBackupPrivateKeyFromSecretStorage: decryption key does not match backup info");
|
||||
}
|
||||
|
||||
await this.backupManager.saveBackupDecryptionKey(backupDecryptionKey, keyBackupInfo.version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status of key backup.
|
||||
*
|
||||
@@ -1211,6 +1235,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
return await this.backupManager.getActiveBackupVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getKeyBackupInfo}.
|
||||
*/
|
||||
public async getKeyBackupInfo(): Promise<KeyBackupInfo | null> {
|
||||
return (await this.backupManager.getServerBackupInfo()) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a key backup can be trusted.
|
||||
*
|
||||
@@ -1280,6 +1311,53 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
obj.signatures = Object.fromEntries(sigs.entries());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#restoreKeyBackupWithPassphrase}.
|
||||
*/
|
||||
public async restoreKeyBackupWithPassphrase(
|
||||
passphrase: string,
|
||||
opts?: KeyBackupRestoreOpts,
|
||||
): Promise<KeyBackupRestoreResult> {
|
||||
const backupInfo = await this.backupManager.getServerBackupInfo();
|
||||
if (!backupInfo?.version) {
|
||||
throw new Error("No backup info available");
|
||||
}
|
||||
|
||||
const privateKey = await keyFromAuthData(backupInfo.auth_data, passphrase);
|
||||
|
||||
// Cache the key
|
||||
await this.storeSessionBackupPrivateKey(privateKey, backupInfo.version);
|
||||
return this.restoreKeyBackup(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#restoreKeyBackup}.
|
||||
*/
|
||||
public async restoreKeyBackup(opts?: KeyBackupRestoreOpts): Promise<KeyBackupRestoreResult> {
|
||||
// Get the decryption key from the crypto store
|
||||
const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys();
|
||||
const { decryptionKey, backupVersion } = backupKeys;
|
||||
if (!decryptionKey || !backupVersion) throw new Error("No decryption key found in crypto store");
|
||||
|
||||
const decodedDecryptionKey = decodeBase64(decryptionKey.toBase64());
|
||||
|
||||
const backupInfo = await this.backupManager.requestKeyBackupVersion(backupVersion);
|
||||
if (!backupInfo) throw new Error(`Backup version to restore ${backupVersion} not found on server`);
|
||||
|
||||
const backupDecryptor = await this.getBackupDecryptor(backupInfo, decodedDecryptionKey);
|
||||
|
||||
try {
|
||||
opts?.progressCallback?.({
|
||||
stage: "fetch",
|
||||
});
|
||||
|
||||
return await this.backupManager.restoreKeyBackup(backupVersion, backupDecryptor, opts);
|
||||
} finally {
|
||||
// Free to avoid to keep in memory the decryption key stored in it. To avoid to exposing it to an attacker.
|
||||
backupDecryptor.free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#isDehydrationSupported}.
|
||||
*/
|
||||
@@ -1959,7 +2037,7 @@ class EventDecryptor {
|
||||
errorDetails,
|
||||
);
|
||||
|
||||
case RustSdkCryptoJs.DecryptionErrorCode.SenderIdentityPreviouslyVerified:
|
||||
case RustSdkCryptoJs.DecryptionErrorCode.SenderIdentityVerificationViolation:
|
||||
// We're refusing to decrypt due to not trusting the sender,
|
||||
// rather than failing to decrypt due to lack of keys, so we
|
||||
// don't need to keep it on the pending list.
|
||||
@@ -2104,22 +2182,29 @@ function rustEncryptionInfoToJsEncryptionInfo(
|
||||
}
|
||||
|
||||
let shieldReason: EventShieldReason | null;
|
||||
if (shieldState.message === undefined) {
|
||||
shieldReason = null;
|
||||
} else if (shieldState.message === "Encrypted by an unverified user.") {
|
||||
// this case isn't actually used with lax shield semantics.
|
||||
shieldReason = EventShieldReason.UNVERIFIED_IDENTITY;
|
||||
} else if (shieldState.message === "Encrypted by a device not verified by its owner.") {
|
||||
shieldReason = EventShieldReason.UNSIGNED_DEVICE;
|
||||
} else if (
|
||||
shieldState.message === "The authenticity of this encrypted message can't be guaranteed on this device."
|
||||
) {
|
||||
shieldReason = EventShieldReason.AUTHENTICITY_NOT_GUARANTEED;
|
||||
} else if (shieldState.message === "Encrypted by an unknown or deleted device.") {
|
||||
shieldReason = EventShieldReason.UNKNOWN_DEVICE;
|
||||
} else {
|
||||
logger.warn(`Unknown shield state message '${shieldState.message}'`);
|
||||
shieldReason = EventShieldReason.UNKNOWN;
|
||||
switch (shieldState.code) {
|
||||
case undefined:
|
||||
case null:
|
||||
shieldReason = null;
|
||||
break;
|
||||
case RustSdkCryptoJs.ShieldStateCode.AuthenticityNotGuaranteed:
|
||||
shieldReason = EventShieldReason.AUTHENTICITY_NOT_GUARANTEED;
|
||||
break;
|
||||
case RustSdkCryptoJs.ShieldStateCode.UnknownDevice:
|
||||
shieldReason = EventShieldReason.UNKNOWN_DEVICE;
|
||||
break;
|
||||
case RustSdkCryptoJs.ShieldStateCode.UnsignedDevice:
|
||||
shieldReason = EventShieldReason.UNSIGNED_DEVICE;
|
||||
break;
|
||||
case RustSdkCryptoJs.ShieldStateCode.UnverifiedIdentity:
|
||||
shieldReason = EventShieldReason.UNVERIFIED_IDENTITY;
|
||||
break;
|
||||
case RustSdkCryptoJs.ShieldStateCode.SentInClear:
|
||||
shieldReason = EventShieldReason.SENT_IN_CLEAR;
|
||||
break;
|
||||
case RustSdkCryptoJs.ShieldStateCode.VerificationViolation:
|
||||
shieldReason = EventShieldReason.VERIFICATION_VIOLATION;
|
||||
break;
|
||||
}
|
||||
|
||||
return { shieldColour, shieldReason };
|
||||
|
||||
@@ -381,8 +381,8 @@ export class RustVerificationRequest
|
||||
* @param qrCodeData - the decoded QR code.
|
||||
* @returns A verifier; call `.verify()` on it to wait for the other side to complete the verification flow.
|
||||
*/
|
||||
public async scanQRCode(uint8Array: Uint8Array): Promise<Verifier> {
|
||||
const scan = RustSdkCryptoJs.QrCodeScan.fromBytes(new Uint8ClampedArray(uint8Array));
|
||||
public async scanQRCode(uint8Array: Uint8ClampedArray): Promise<Verifier> {
|
||||
const scan = RustSdkCryptoJs.QrCodeScan.fromBytes(uint8Array);
|
||||
const verifier: RustSdkCryptoJs.Qr = await this.inner.scanQrCode(scan);
|
||||
|
||||
// this should have triggered the onChange callback, and we should now have a verifier
|
||||
@@ -416,7 +416,7 @@ export class RustVerificationRequest
|
||||
/**
|
||||
* Stub implementation of {@link Crypto.VerificationRequest#getQRCodeBytes}.
|
||||
*/
|
||||
public getQRCodeBytes(): Buffer | undefined {
|
||||
public getQRCodeBytes(): Uint8ClampedArray | undefined {
|
||||
throw new Error("getQRCodeBytes() unsupported in Rust Crypto; use generateQRCode() instead.");
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ export class RustVerificationRequest
|
||||
*
|
||||
* Implementation of {@link Crypto.VerificationRequest#generateQRCode}.
|
||||
*/
|
||||
public async generateQRCode(): Promise<Buffer | undefined> {
|
||||
public async generateQRCode(): Promise<Uint8ClampedArray | undefined> {
|
||||
// make sure that we have a list of the other user's devices (workaround https://github.com/matrix-org/matrix-rust-sdk/issues/2896)
|
||||
if (!(await this.getOtherDevice())) {
|
||||
throw new Error("generateQRCode(): other device is unknown");
|
||||
@@ -435,7 +435,7 @@ export class RustVerificationRequest
|
||||
// If we are unable to generate a QRCode, we return undefined
|
||||
if (!innerVerifier) return;
|
||||
|
||||
return Buffer.from(innerVerifier.toBytes());
|
||||
return innerVerifier.toBytes();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user