Compare commits
177 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c4048d985d | |||
| a8f8a9c14d | |||
| 0e2f73d7a7 | |||
| 31aeb3044f | |||
| 0a29063bc9 | |||
| 3cc3bd0728 | |||
| f891fe4423 | |||
| b99ff83785 | |||
| 23c4c9fd8a | |||
| 8b8ee91210 | |||
| d8c431f23e | |||
| a6fb7530cb | |||
| b5c3f15a67 | |||
| 91f6f0f9c5 | |||
| 88cf5eb769 | |||
| 13a967ae8f | |||
| 66c80949e8 | |||
| 3d51e31da8 | |||
| 86c190fa0d | |||
| e9a9280e3c | |||
| fabdd6da64 | |||
| ab5d95102c | |||
| b4556d6552 | |||
| 6c22da9a96 | |||
| d29329bddb | |||
| 68b06d7b60 | |||
| 5f599ee978 | |||
| aabd558bce | |||
| 662b772c73 | |||
| b240c44128 | |||
| 5508993d79 | |||
| c9b43ab251 | |||
| 2fb1e659c8 | |||
| b842192a34 | |||
| 868bbfcb31 | |||
| 860161bdd2 | |||
| 0c9d82e40a | |||
| 73ab7c342a | |||
| 3386c66b98 | |||
| da044820d7 | |||
| 9f40f32b3e | |||
| f679942584 | |||
| c1b4f97372 | |||
| 5f3b89990d | |||
| 866fd6f4a3 | |||
| 9ecb66e695 | |||
| baa6d13506 | |||
| 2d6230f199 | |||
| 825d85f18d | |||
| 1dcb7a6e75 | |||
| 823316b2ff | |||
| d56fa197d0 | |||
| f7229bfff0 | |||
| 538717c23e | |||
| 1a8ea3d685 | |||
| d0890d9450 | |||
| 42ec17b977 | |||
| 82e9eefce6 | |||
| f8208b1891 | |||
| 092a59af66 | |||
| dbd7d26968 | |||
| 4fda9e8419 | |||
| a13e0389db | |||
| dbb4828eda | |||
| 414ac9d8cc | |||
| 30058a4bdc | |||
| fab9cab3df | |||
| 53b599f8fe | |||
| 17f6cc733e | |||
| c8403f39aa | |||
| 8cf5df73ee | |||
| febe27ddcc | |||
| 7987ce76ec | |||
| 60cedf2fdb | |||
| ed44514974 | |||
| 9f8c1ee953 | |||
| 593a57fc2b | |||
| e8128d34a1 | |||
| ba7bd06295 | |||
| e4db6008b8 | |||
| 52f35409ec | |||
| f50aab37c3 | |||
| df0f817f83 | |||
| 7efa6352f8 | |||
| f74614705e | |||
| 169e8f8613 | |||
| f2f77bd1f7 | |||
| 5a1c70ad19 | |||
| 27cb16ffe4 | |||
| 9be0b3e701 | |||
| 05ba27f36b | |||
| e6acfdf275 | |||
| 2a6612c73a | |||
| 2f8b05b0da | |||
| fe984ede6e | |||
| 3f74b9a0cc | |||
| 802b996b10 | |||
| 8d44f9d665 | |||
| a72a1b294a | |||
| ab5f32f984 | |||
| 6a21d812ab | |||
| ee94e93354 | |||
| 31c4786a96 | |||
| 17b6e59819 | |||
| 42510022a1 | |||
| 5f0978ac3f | |||
| cd6787e0ac | |||
| 03baa3e358 | |||
| 658563e2a7 | |||
| 26d3033b17 | |||
| a4bd7dc7d7 | |||
| 1f48544b38 | |||
| 7ef4062f59 | |||
| 968bc51a35 | |||
| d413f5042e | |||
| b8e8b14375 | |||
| 43e58871de | |||
| 2544c14032 | |||
| 8d19782c57 | |||
| 340bbe1a8f | |||
| 8214fd7156 | |||
| a0efed8b88 | |||
| c408c0d1d5 | |||
| d6080398db | |||
| 467908703b | |||
| 87eddaf51a | |||
| c65ef03567 | |||
| 78cbf7cd28 | |||
| dc7c1a4fef | |||
| 1ae0c2f3ee | |||
| affaa95fb4 | |||
| 9176d3a671 | |||
| de50129a53 | |||
| 5568dfdd41 | |||
| 39216d44ed | |||
| 8c3b249567 | |||
| b8e40ad2a8 | |||
| 4e2831764d | |||
| 09780672aa | |||
| 0fe53876ec | |||
| dfec3dc33c | |||
| fbdd78b428 | |||
| e10c362ef0 | |||
| 89a9a7fa38 | |||
| 687d08dc9d | |||
| 7f91db83d0 | |||
| 0300d6343f | |||
| e0ef467d7d | |||
| dc1cccfecc | |||
| 79299891fd | |||
| d32f398345 | |||
| 0f08c00c07 | |||
| 6b261b98c9 | |||
| 99f157a0f1 | |||
| f9f6d81346 | |||
| 46604abe7b | |||
| 553758e713 | |||
| 509e64cfc1 | |||
| 60c2e9b3ed | |||
| cfb21fa80a | |||
| c3d7f4e730 | |||
| aa97beae44 | |||
| 5feab37166 | |||
| 1a02835ab2 | |||
| 6f63ff1711 | |||
| 4d90fecb6a | |||
| 30a26813ec | |||
| f17a4fedb9 | |||
| 94e393c9a6 | |||
| 53201688a6 | |||
| 996663bf64 | |||
| d6e4338a37 | |||
| b2665f2128 | |||
| af4b6bc126 | |||
| 565bb0ef7c | |||
| fe0edcd081 | |||
| 51544f25a7 |
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "import", "jsdoc"],
|
||||
plugins: ["matrix-org", "import", "jsdoc", "n"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
@@ -108,10 +108,8 @@ module.exports = {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
// The non-TypeScript rule produces false positives
|
||||
"func-call-spacing": "off",
|
||||
"@typescript-eslint/func-call-spacing": ["error"],
|
||||
// We do this sometimes to brand interfaces
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
@@ -128,6 +126,14 @@ module.exports = {
|
||||
// These need a bit more work before we can enable
|
||||
// "jsdoc/check-param-names": "error",
|
||||
// "jsdoc/check-indentation": "error",
|
||||
// Ensure .ts extension on imports outside of tests
|
||||
"n/file-extension-in-import": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
tryExtensions: [".ts"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2,6 +2,7 @@
|
||||
/.github/workflows/** @matrix-org/element-web-team
|
||||
/package.json @matrix-org/element-web-team
|
||||
/yarn.lock @matrix-org/element-web-team
|
||||
/scripts/** @matrix-org/element-web-team
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/src/matrixrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
|
||||
@@ -22,7 +22,7 @@ runs:
|
||||
|
||||
- name: Upload tarball signature
|
||||
if: ${{ inputs.upload-url }}
|
||||
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # 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@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}.asc
|
||||
|
||||
- name: Upload assets
|
||||
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
jobs:
|
||||
netlify:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
@@ -21,14 +21,13 @@ concurrency:
|
||||
jobs:
|
||||
playwright:
|
||||
name: Playwright
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
|
||||
uses: element-hq/element-web/.github/workflows/end-to-end-tests.yaml@develop
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume multiple concurrent runners.
|
||||
skip: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
@@ -14,10 +14,8 @@ jobs:
|
||||
include:
|
||||
- repo: element-hq/element-web
|
||||
event: element-web-notify
|
||||
- repo: matrix-org/matrix-react-sdk
|
||||
event: upstream-sdk-notify
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
||||
|
||||
@@ -12,9 +12,9 @@ concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || gi
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@5847eef68201219cf0a4643ea7be61e77837bbce # v5
|
||||
- uses: mheap/github-action-required-labels@d25134c992b943fb6ad00c25ea00eb5988c0a9dd # v5
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
labels: |
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
community-prs:
|
||||
name: Label Community PRs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
concurrency: release-drafter-action
|
||||
jobs:
|
||||
draft:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
script: |
|
||||
const { RELEASE_ID: releaseId, DEPENDENCY, VERSION } = process.env;
|
||||
const { owner, repo } = context.repo;
|
||||
const script = require("./.action-repo/scripts/release/merge-release-notes.js");
|
||||
const script = require("./.action-repo/scripts/release/merge-release-notes.cjs");
|
||||
|
||||
let deps = [];
|
||||
if (DEPENDENCY.includes("/")) {
|
||||
|
||||
@@ -15,7 +15,7 @@ on:
|
||||
concurrency: ${{ github.workflow }}
|
||||
jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -34,6 +34,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
@@ -60,26 +61,25 @@ jobs:
|
||||
CURRENT_VERSION=$(cat package.json | jq -r .dependencies[\"$PACKAGE\"])
|
||||
echo "Current $PACKAGE version is $CURRENT_VERSION"
|
||||
|
||||
if [ "$CURRENT_VERSION" == "null" ]
|
||||
if [[ "$CURRENT_VERSION" == "null" ]]
|
||||
then
|
||||
echo "Unable to find $PACKAGE in package.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_VERSION" == "develop" ]
|
||||
if [[ "$CURRENT_VERSION" == *"#develop" ]]
|
||||
then
|
||||
echo "Not updating dependency $PACKAGE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Resetting $1 to develop branch..."
|
||||
echo "Resetting $PACKAGE to develop branch..."
|
||||
yarn add "github:matrix-org/$PACKAGE#develop"
|
||||
git add -u
|
||||
git commit -m "Reset $PACKAGE back to develop branch"
|
||||
done <<< "$DEPENDENCIES"
|
||||
env:
|
||||
DEPENDENCIES: ${{ inputs.dependencies }}
|
||||
FINAL: ${{ inputs.final }}
|
||||
|
||||
- name: Push changes
|
||||
run: git push origin develop
|
||||
|
||||
@@ -41,7 +41,7 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
environment: Release
|
||||
steps:
|
||||
- name: Load GPG key
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
post-release:
|
||||
name: Post release steps
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- id: repository
|
||||
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||
@@ -304,7 +304,7 @@ jobs:
|
||||
bump-downstreams:
|
||||
name: Update npm dependency in downstream projects
|
||||
needs: npm
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: inputs.downstreams
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
id: ${{ steps.npm-publish.outputs.id }}
|
||||
steps:
|
||||
|
||||
@@ -28,13 +28,13 @@ jobs:
|
||||
with:
|
||||
final: ${{ inputs.mode == 'final' }}
|
||||
npm: ${{ inputs.npm }}
|
||||
downstreams: '["matrix-org/matrix-react-sdk", "element-hq/element-web"]'
|
||||
downstreams: '["element-hq/element-web"]'
|
||||
|
||||
docs:
|
||||
name: Publish Documentation
|
||||
needs: release
|
||||
if: inputs.docs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
needs: docs
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
|
||||
@@ -14,7 +14,7 @@ on:
|
||||
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event != 'merge_group'
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v3.2
|
||||
uses: matrix-org/sonarcloud-workflow-action@v3.3
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
|
||||
@@ -11,7 +11,7 @@ concurrency:
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
workflow_lint:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
|
||||
analyse_dead_code:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@ env:
|
||||
jobs:
|
||||
jest:
|
||||
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [integ, unit]
|
||||
node: ["lts/*", 21, 22]
|
||||
node: ["lts/*", 22]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -63,10 +63,20 @@ jobs:
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
matrix-react-sdk:
|
||||
name: Downstream test matrix-react-sdk
|
||||
# Dummy completion job to simplify branch protections
|
||||
jest-complete:
|
||||
name: Jest tests
|
||||
needs: jest
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
element-web:
|
||||
name: Downstream test element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/tests.yml@develop
|
||||
uses: element-hq/element-web/.github/workflows/tests.yml@develop
|
||||
with:
|
||||
disable_coverage: true
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
@@ -81,7 +91,7 @@ jobs:
|
||||
# we need this so the job is reported properly when run in a merge queue
|
||||
downstream-complement-crypto:
|
||||
name: Downstream Complement Crypto tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- complement-crypto
|
||||
@@ -93,10 +103,10 @@ jobs:
|
||||
# and skip sonarcloud coverage within merge queues
|
||||
downstream:
|
||||
name: Downstream tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- matrix-react-sdk
|
||||
- element-web
|
||||
steps:
|
||||
- name: Skip SonarCloud on merge queues
|
||||
if: env.ENABLE_COVERAGE == 'false'
|
||||
@@ -109,5 +119,5 @@ jobs:
|
||||
sha: ${{ github.sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- if: needs.matrix-react-sdk.result != 'skipped' && needs.matrix-react-sdk.result != 'success'
|
||||
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
automate-project-columns-next:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
|
||||
+136
@@ -1,3 +1,139 @@
|
||||
Changes in [34.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.10.0) (2024-11-05)
|
||||
====================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate `CreateSecretStorageOpts.keyBackupInfo` used in `CryptoApi.bootstrapSecretStorage.` ([#4474](https://github.com/matrix-org/matrix-js-sdk/pull/4474)). Contributed by @florianduros.
|
||||
* Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() ([#4380](https://github.com/matrix-org/matrix-js-sdk/pull/4380)). Contributed by @hughns.
|
||||
* Remove abandoned MSC3886, MSC3903, MSC3906 experimental implementations ([#4469](https://github.com/matrix-org/matrix-js-sdk/pull/4469)). Contributed by @t3chguy.
|
||||
* Deprecate `MatrixClient.getDehydratedDevice` ([#4467](https://github.com/matrix-org/matrix-js-sdk/pull/4467)). Contributed by @florianduros.
|
||||
* Deprecate top level crypto events re-export ([#4444](https://github.com/matrix-org/matrix-js-sdk/pull/4444)). Contributed by @florianduros.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() ([#4380](https://github.com/matrix-org/matrix-js-sdk/pull/4380)). Contributed by @hughns.
|
||||
* Do not rotate MatrixRTC media encryption key when a new member joins a session ([#4472](https://github.com/matrix-org/matrix-js-sdk/pull/4472)). Contributed by @hughns.
|
||||
* Avoid `<sender>|<session>` notation in log messages ([#4473](https://github.com/matrix-org/matrix-js-sdk/pull/4473)). Contributed by @richvdh.
|
||||
* Refactor/simplify Promises in MatrixRTCSession ([#4466](https://github.com/matrix-org/matrix-js-sdk/pull/4466)). Contributed by @AndrewFerr.
|
||||
* Prepare delayed call leave events more reliably ([#4447](https://github.com/matrix-org/matrix-js-sdk/pull/4447)). Contributed by @AndrewFerr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix DelayedEventInfo type ([#4446](https://github.com/matrix-org/matrix-js-sdk/pull/4446)). Contributed by @AndrewFerr.
|
||||
|
||||
|
||||
Changes in [34.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.9.0) (2024-10-22)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate the crypto events which are not used by the rust-crypto ([#4442](https://github.com/matrix-org/matrix-js-sdk/pull/4442)). Contributed by @florianduros.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix the rust crypto import in esm environments. ([#4445](https://github.com/matrix-org/matrix-js-sdk/pull/4445)). Contributed by @saul-jb.
|
||||
* Fix MatrixRTC sender key wrapping ([#4441](https://github.com/matrix-org/matrix-js-sdk/pull/4441)). Contributed by @hughns.
|
||||
|
||||
|
||||
Changes in [34.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.8.0) (2024-10-15)
|
||||
==================================================================================================
|
||||
This release removes insecure functionality, resolving CVE-2024-47080 / GHSA-4jf8-g8wp-cx7c.
|
||||
|
||||
Changes in [34.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.7.0) (2024-10-08)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys() ([#4427](https://github.com/matrix-org/matrix-js-sdk/pull/4427)). Contributed by @hughns.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Bump matrix-rust-sdk to 9.1.0 ([#4435](https://github.com/matrix-org/matrix-js-sdk/pull/4435)). Contributed by @richvdh.
|
||||
* Rotate Matrix RTC media encryption key when a new member joins a call for Post Compromise Security ([#4422](https://github.com/matrix-org/matrix-js-sdk/pull/4422)). Contributed by @hughns.
|
||||
* Update media event content types to include captions ([#4403](https://github.com/matrix-org/matrix-js-sdk/pull/4403)). Contributed by @tulir.
|
||||
* Update OIDC registration types to match latest MSC2966 state ([#4432](https://github.com/matrix-org/matrix-js-sdk/pull/4432)). Contributed by @t3chguy.
|
||||
* Add `CryptoApi.pinCurrentUserIdentity` and `UserIdentity.needsUserApproval` ([#4415](https://github.com/matrix-org/matrix-js-sdk/pull/4415)). Contributed by @richvdh.
|
||||
|
||||
|
||||
Changes in [34.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.6.0) (2024-09-24)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Element-R: Mark unsupported MatrixClient methods as deprecated ([#4389](https://github.com/matrix-org/matrix-js-sdk/pull/4389)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add crypto mode setting for invisible crypto, and apply it to decrypting events ([#4407](https://github.com/matrix-org/matrix-js-sdk/pull/4407)). Contributed by @uhoreg.
|
||||
* Don't share full key history for RTC per-participant encryption ([#4406](https://github.com/matrix-org/matrix-js-sdk/pull/4406)). Contributed by @hughns.
|
||||
* Export membership types ([#4405](https://github.com/matrix-org/matrix-js-sdk/pull/4405)). Contributed by @Johennes.
|
||||
* Fix sending redacts in embedded (widget) mode ([#4398](https://github.com/matrix-org/matrix-js-sdk/pull/4398)). Contributed by @toger5.
|
||||
* Expose the event ID of a call membership ([#4395](https://github.com/matrix-org/matrix-js-sdk/pull/4395)). Contributed by @robintown.
|
||||
* MSC4133 - Extended profiles ([#4391](https://github.com/matrix-org/matrix-js-sdk/pull/4391)). Contributed by @Half-Shot.
|
||||
|
||||
|
||||
Changes in [34.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.5.0) (2024-09-10)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate unused callback hooks `CryptoCallbacks.onSecretRequested` and `CryptoCallbacks.getDehydrationKey` ([#4376](https://github.com/matrix-org/matrix-js-sdk/pull/4376)). Contributed by @richvdh.
|
||||
|
||||
|
||||
Changes in [34.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.4.0) (2024-08-27)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Use non-legacy calls if any are found ([#4337](https://github.com/matrix-org/matrix-js-sdk/pull/4337)). Contributed by @AndrewFerr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Retry event decryption failures on first failure ([#4346](https://github.com/matrix-org/matrix-js-sdk/pull/4346)). Contributed by @hughns.
|
||||
* Ensure "type" = "module" ES declaration in pre-release.sh ([#4350](https://github.com/matrix-org/matrix-js-sdk/pull/4350)). Contributed by @BLCK-B.
|
||||
* Handle MatrixRTC encryption keys arriving out of order ([#4345](https://github.com/matrix-org/matrix-js-sdk/pull/4345)). Contributed by @hughns.
|
||||
* Resend MatrixRTC encryption keys if a membership has changed ([#4343](https://github.com/matrix-org/matrix-js-sdk/pull/4343)). Contributed by @hughns.
|
||||
|
||||
|
||||
Changes in [34.3.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.3.1) (2024-08-20)
|
||||
==================================================================================================
|
||||
# Security
|
||||
- Fixes for [CVE-2024-42369](https://nvd.nist.gov/vuln/detail/CVE-2024-42369) / [GHSA-vhr5-g3pm-49fm](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-vhr5-g3pm-49fm).
|
||||
|
||||
Changes in [34.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.3.0) (2024-08-13)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Bump matrix-widget-api ([#4336](https://github.com/matrix-org/matrix-js-sdk/pull/4336)). Contributed by @AndrewFerr.
|
||||
* Also check for MSC3757 for session state keys ([#4334](https://github.com/matrix-org/matrix-js-sdk/pull/4334)). Contributed by @AndrewFerr.
|
||||
* Support Futures via widgets ([#4311](https://github.com/matrix-org/matrix-js-sdk/pull/4311)). Contributed by @AndrewFerr.
|
||||
* Support MSC4140: Delayed events (Futures) ([#4294](https://github.com/matrix-org/matrix-js-sdk/pull/4294)). Contributed by @AndrewFerr.
|
||||
* Handle late-arriving `m.room_key.withheld` messages ([#4310](https://github.com/matrix-org/matrix-js-sdk/pull/4310)). Contributed by @richvdh.
|
||||
* Be specific about what is considered a MSC4143 call member event. ([#4328](https://github.com/matrix-org/matrix-js-sdk/pull/4328)). Contributed by @toger5.
|
||||
* Add index.ts for matrixrtc module ([#4314](https://github.com/matrix-org/matrix-js-sdk/pull/4314)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix hashed ID server lookups with no Olm ([#4333](https://github.com/matrix-org/matrix-js-sdk/pull/4333)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [34.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.2.0) (2024-07-30)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Element-R: detect "withheld key" UTD errors, and mark them as such ([#4302](https://github.com/matrix-org/matrix-js-sdk/pull/4302)). Contributed by @richvdh.
|
||||
|
||||
|
||||
Changes in [34.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.1.0) (2024-07-16)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Add ability to choose how many timeline events to sync when peeking ([#4300](https://github.com/matrix-org/matrix-js-sdk/pull/4300)). Contributed by @jgarplind.
|
||||
* Remove redundant hack for using the old pickle key in rust crypto ([#4282](https://github.com/matrix-org/matrix-js-sdk/pull/4282)). Contributed by @richvdh.
|
||||
* Add fetching the well known in embedded mode. ([#4259](https://github.com/matrix-org/matrix-js-sdk/pull/4259)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix room state being updated with old (now overwritten) state and emitting for those updates. ([#4242](https://github.com/matrix-org/matrix-js-sdk/pull/4242)). Contributed by @toger5.
|
||||
* Fix incorrect "Olm is not available" errors ([#4301](https://github.com/matrix-org/matrix-js-sdk/pull/4301)). Contributed by @richvdh.
|
||||
* Fix build for example script ([#4286](https://github.com/matrix-org/matrix-js-sdk/pull/4286)). Contributed by @richvdh.
|
||||
* Declare matrix-js-sdk as an ES module ([#4285](https://github.com/matrix-org/matrix-js-sdk/pull/4285)). Contributed by @richvdh.
|
||||
|
||||
|
||||
Changes in [34.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.0.0) (2024-07-08)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
@@ -39,7 +39,7 @@ 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) for a more complex example.
|
||||
[the Node.js terminal app](examples/node/README.md) for a more complex example.
|
||||
|
||||
To start the client:
|
||||
|
||||
@@ -191,6 +191,7 @@ As well as the primary entry point (`matrix-js-sdk`), there are several other en
|
||||
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
|
||||
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
|
||||
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
|
||||
| `matrix-js-sdk/lib/utils/*.js` | A set of modules exporting standalone functions (and their types). |
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
+27
-1
@@ -14,7 +14,16 @@ module.exports = {
|
||||
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
|
||||
},
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
[
|
||||
"@babel/preset-typescript",
|
||||
{
|
||||
// When using the transpiled javascript in `lib`, Node.js requires `.js` extensions on any `import`
|
||||
// specifiers. However, Jest uses the TS source (via babel) and fails to resolve the `.js` names.
|
||||
// To resolve this,we use the `.ts` names in the source, and rewrite the `import` specifiers to use
|
||||
// `.js` during transpilation, *except* when we are targetting Jest.
|
||||
rewriteImportExtensions: process.env.NODE_ENV !== "test",
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
"@babel/plugin-transform-numeric-separator",
|
||||
@@ -22,5 +31,22 @@ module.exports = {
|
||||
"@babel/plugin-transform-object-rest-spread",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime",
|
||||
[
|
||||
"search-and-replace",
|
||||
{
|
||||
// Since rewriteImportExtensions doesn't work on dynamic imports (yet), we need to manually replace
|
||||
// the dynamic rust-crypto import.
|
||||
// (see https://github.com/babel/babel/issues/16750)
|
||||
rules:
|
||||
process.env.NODE_ENV !== "test"
|
||||
? [
|
||||
{
|
||||
search: "./rust-crypto/index.ts",
|
||||
replace: "./rust-crypto/index.js",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
+16
-11
@@ -1,9 +1,15 @@
|
||||
import clc from "cli-color";
|
||||
import fs from "fs";
|
||||
import readline from "readline";
|
||||
import sdk, { ClientEvent, EventType, MsgType, RoomEvent } from "matrix-js-sdk";
|
||||
import { KnownMembership } from "matrix-js-sdk/lib/@types/membership.js";
|
||||
|
||||
var myHomeServer = "http://localhost:8008";
|
||||
var myUserId = "@example:localhost";
|
||||
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
var sdk = require("matrix-js-sdk");
|
||||
var clc = require("cli-color");
|
||||
|
||||
var matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
baseUrl: myHomeServer,
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
});
|
||||
@@ -15,7 +21,6 @@ var numMessagesToShow = 20;
|
||||
|
||||
// Reading from stdin
|
||||
var CLEAR_CONSOLE = "\x1B[2J";
|
||||
var readline = require("readline");
|
||||
var rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
@@ -97,7 +102,7 @@ rl.on("line", function (line) {
|
||||
})
|
||||
.then(function (url) {
|
||||
var content = {
|
||||
msgtype: "m.file",
|
||||
msgtype: MsgType.File,
|
||||
body: filename,
|
||||
url: JSON.parse(url).content_uri,
|
||||
};
|
||||
@@ -138,7 +143,7 @@ rl.on("line", function (line) {
|
||||
// ==== END User input
|
||||
|
||||
// show the room list after syncing.
|
||||
matrixClient.on("sync", function (state, prevState, data) {
|
||||
matrixClient.on(ClientEvent.Sync, function (state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
setRoomList();
|
||||
@@ -149,7 +154,7 @@ matrixClient.on("sync", function (state, prevState, data) {
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.on("Room", function () {
|
||||
matrixClient.on(ClientEvent.Room, function () {
|
||||
setRoomList();
|
||||
if (!viewingRoom) {
|
||||
printRoomList();
|
||||
@@ -158,7 +163,7 @@ matrixClient.on("Room", function () {
|
||||
});
|
||||
|
||||
// print incoming messages.
|
||||
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
|
||||
matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) {
|
||||
return; // don't print paginated results
|
||||
}
|
||||
@@ -305,7 +310,7 @@ function printRoomInfo(room) {
|
||||
print(eTypeHeader + sendHeader + contentHeader);
|
||||
print(new Array(100).join("-"));
|
||||
eventMap.keys().forEach(function (eventType) {
|
||||
if (eventType === "m.room.member") {
|
||||
if (eventType === EventType.RoomMember) {
|
||||
return;
|
||||
} // use /members instead.
|
||||
var eventEventMap = eventMap.get(eventType);
|
||||
@@ -343,7 +348,7 @@ function printLine(event) {
|
||||
name = name.slice(0, maxNameWidth - 1) + "\u2026";
|
||||
}
|
||||
|
||||
if (event.getType() === "m.room.message") {
|
||||
if (event.getType() === EventType.RoomMessage) {
|
||||
body = event.getContent().body;
|
||||
} else if (event.isState()) {
|
||||
var stateName = event.getType();
|
||||
@@ -394,4 +399,4 @@ function fixWidth(str, len) {
|
||||
return str;
|
||||
}
|
||||
|
||||
matrixClient.startClient(numMessagesToShow); // messages for each room.
|
||||
matrixClient.startClient({ initialSyncLimit: numMessagesToShow });
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
"main": "app.js",
|
||||
"author": "",
|
||||
"license": "Apache 2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"cli-color": "^1.0.0",
|
||||
"matrix-js-sdk": "^32.0.0"
|
||||
"matrix-js-sdk": "^34.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ if (env["GITHUB_ACTIONS"] !== undefined) {
|
||||
|
||||
// if we're running against the develop branch, also enable the slow test reporter
|
||||
if (env["GITHUB_REF"] == "refs/heads/develop") {
|
||||
reporters.push("<rootDir>/spec/slowReporter.js");
|
||||
reporters.push("<rootDir>/spec/slowReporter.cjs");
|
||||
}
|
||||
config.reporters = reporters;
|
||||
}
|
||||
|
||||
+19
-20
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "34.1.0-rc.3",
|
||||
"version": "34.10.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -31,13 +31,10 @@
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "./lib/index.js",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.ts",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"matrix_lib_browser": "./lib/browser-index.js",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
@@ -53,7 +50,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^6.0.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
@@ -61,7 +58,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.6.0",
|
||||
"matrix-widget-api": "^1.8.2",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
@@ -84,6 +81,7 @@
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@peculiar/webcrypto": "^1.4.5",
|
||||
"@stylistic/eslint-plugin": "^2.9.0",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
@@ -91,22 +89,24 @@
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babel-plugin-search-and-replace": "^1.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"eslint": "8.57.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
"eslint-plugin-jsdoc": "^48.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"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-unicorn": "^54.0.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "10.0.7",
|
||||
"fetch-mock": "11.1.5",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^29.0.0",
|
||||
@@ -117,19 +117,18 @@
|
||||
"lint-staged": "^15.0.2",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.3.2",
|
||||
"rimraf": "^5.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"rimraf": "^6.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.0",
|
||||
"typedoc-plugin-coverage": "^3.0.0",
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
"typedoc-plugin-missing-exports": "^3.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fsProm = require("fs/promises");
|
||||
|
||||
const PKGJSON = "package.json";
|
||||
|
||||
async function main() {
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
|
||||
for (const field of ["main", "typings"]) {
|
||||
if (pkgJson["matrix_lib_" + field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_" + field];
|
||||
}
|
||||
}
|
||||
|
||||
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fsProm = require("fs/promises");
|
||||
|
||||
const PKGJSON = "package.json";
|
||||
|
||||
async function main() {
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
|
||||
for (const field of ["main", "typings"]) {
|
||||
if (pkgJson["matrix_lib_" + field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_" + field];
|
||||
}
|
||||
}
|
||||
|
||||
// matrix-js-sdk is built into ECMAScript modules. Make sure we declare it as such.
|
||||
// See https://nodejs.org/api/packages.html#type.
|
||||
pkgJson["type"] = "module";
|
||||
|
||||
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
+1
@@ -0,0 +1 @@
|
||||
switch_package_to_release.cjs
|
||||
@@ -21,7 +21,7 @@ import { IDBFactory } from "fake-indexeddb";
|
||||
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
|
||||
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
|
||||
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||
@@ -169,17 +169,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
|
||||
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
|
||||
const masterKey = await encryptAES(
|
||||
const masterKey = await encryptAESSecretStorageItem(
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
encryptionKey,
|
||||
"m.cross_signing.master",
|
||||
);
|
||||
const selfSigningKey = await encryptAES(
|
||||
const selfSigningKey = await encryptAESSecretStorageItem(
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
encryptionKey,
|
||||
"m.cross_signing.self_signing",
|
||||
);
|
||||
const userSigningKey = await encryptAES(
|
||||
const userSigningKey = await encryptAESSecretStorageItem(
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
encryptionKey,
|
||||
"m.cross_signing.user_signing",
|
||||
|
||||
@@ -83,9 +83,12 @@ import {
|
||||
CrossSigningKey,
|
||||
CryptoCallbacks,
|
||||
DecryptionFailureCode,
|
||||
DeviceIsolationMode,
|
||||
EventShieldColour,
|
||||
EventShieldReason,
|
||||
KeyBackupInfo,
|
||||
AllDevicesIsolationMode,
|
||||
OnlySignedDevicesIsolationMode,
|
||||
} from "../../../src/crypto-api";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
@@ -746,6 +749,95 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
);
|
||||
});
|
||||
|
||||
describe("IsolationMode decryption tests", () => {
|
||||
newBackendOnly(
|
||||
"OnlySigned mode - fails with an error when cross-signed sender is required but sender is not cross-signed",
|
||||
async () => {
|
||||
const decryptedEvent = await setUpTestAndDecrypt(new OnlySignedDevicesIsolationMode());
|
||||
|
||||
// It will error as an unknown device because we haven't fetched
|
||||
// the sender's device keys.
|
||||
expect(decryptedEvent.isDecryptionFailure()).toBe(true);
|
||||
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
|
||||
},
|
||||
);
|
||||
|
||||
newBackendOnly(
|
||||
"NoIsolation mode - Decrypts with warning when cross-signed sender is required but sender is not cross-signed",
|
||||
async () => {
|
||||
const decryptedEvent = await setUpTestAndDecrypt(new AllDevicesIsolationMode(false));
|
||||
|
||||
expect(decryptedEvent.isDecryptionFailure()).toBe(false);
|
||||
|
||||
expect(await aliceClient.getCrypto()!.getEncryptionInfoForEvent(decryptedEvent)).toEqual({
|
||||
shieldColour: EventShieldColour.RED,
|
||||
shieldReason: EventShieldReason.UNKNOWN_DEVICE,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
async function setUpTestAndDecrypt(isolationMode: DeviceIsolationMode): Promise<MatrixEvent> {
|
||||
// This tests that a message will not be decrypted if the sender
|
||||
// is not sufficiently trusted according to the selected crypto
|
||||
// mode.
|
||||
//
|
||||
// This test is almost the same as the "Alice receives a megolm
|
||||
// message" test, with the main difference that we set the
|
||||
// crypto mode.
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
|
||||
// Start by using Invisible crypto mode
|
||||
aliceClient.getCrypto()!.setDeviceIsolationMode(isolationMode);
|
||||
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
recipient: aliceClient.getUserId()!,
|
||||
recipientCurve25519Key: keyReceiver.getDeviceKey(),
|
||||
recipientEd25519Key: keyReceiver.getSigningKey(),
|
||||
olmAccount: testOlmAccount,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
const messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: { timeline: { events: [messageEncrypted] } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
|
||||
// it probably won't be decrypted yet, because it takes a while to process the olm keys
|
||||
return await testUtils.awaitDecryption(event);
|
||||
}
|
||||
});
|
||||
|
||||
it("Decryption fails with Unable to decrypt for other errors", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
@@ -2333,8 +2425,84 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
|
||||
describe("m.room_key.withheld handling", () => {
|
||||
// TODO: there are a bunch more tests for this sort of thing in spec/unit/crypto/algorithms/megolm.spec.ts.
|
||||
// They should be converted to integ tests and moved.
|
||||
describe.each([
|
||||
["m.blacklisted", "The sender has blocked you.", DecryptionFailureCode.MEGOLM_KEY_WITHHELD],
|
||||
[
|
||||
"m.unverified",
|
||||
"The sender has disabled encrypting to unverified devices.",
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
],
|
||||
])(
|
||||
"Decryption fails with withheld error if a withheld notice with code '%s' is received",
|
||||
(withheldCode, expectedMessage, expectedErrorCode) => {
|
||||
it.each(["before", "after"])("%s the event", async (when) => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
|
||||
let awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
|
||||
|
||||
// Send Alice an encrypted room event which looks like it was encrypted with a megolm session
|
||||
async function sendEncryptedEvent() {
|
||||
const event = {
|
||||
...testData.ENCRYPTED_EVENT,
|
||||
origin_server_ts: Date.now(),
|
||||
};
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: { join: { [ROOM_ID]: { timeline: { events: [event] } } } },
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
}
|
||||
|
||||
// Send Alice a withheld notice
|
||||
async function sendWithheldMessage() {
|
||||
const withheldMessage = {
|
||||
type: "m.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: ROOM_ID,
|
||||
sender_key: testData.ENCRYPTED_EVENT.content!.sender_key,
|
||||
session_id: testData.ENCRYPTED_EVENT.content!.session_id,
|
||||
code: withheldCode,
|
||||
reason: "zzz",
|
||||
},
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
to_device: { events: [withheldMessage] },
|
||||
});
|
||||
await syncPromise(aliceClient);
|
||||
}
|
||||
|
||||
if (when === "before") {
|
||||
await sendWithheldMessage();
|
||||
await sendEncryptedEvent();
|
||||
} else {
|
||||
await sendEncryptedEvent();
|
||||
// Make sure that the first attempt to decrypt has happened before the withheld arrives
|
||||
await awaitDecryption;
|
||||
awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
|
||||
await sendWithheldMessage();
|
||||
}
|
||||
|
||||
const ev = await awaitDecryption;
|
||||
expect(ev.getContent()).toEqual({
|
||||
body: `** Unable to decrypt: DecryptionError: ${expectedMessage} **`,
|
||||
msgtype: "m.bad.encrypted",
|
||||
});
|
||||
|
||||
expect(ev.decryptionFailureReason).toEqual(expectedErrorCode);
|
||||
|
||||
// `isEncryptedDisabledForUnverifiedDevices` should be true for `m.unverified` and false for other errors.
|
||||
expect(ev.isEncryptedDisabledForUnverifiedDevices).toEqual(withheldCode === "m.unverified");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
oldBackendOnly("does not block decryption on an 'm.unavailable' report", async function () {
|
||||
// there may be a key downloads for alice
|
||||
@@ -3103,15 +3271,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
});
|
||||
|
||||
describe("Check if the cross signing keys are available for a user", () => {
|
||||
describe("User identity", () => {
|
||||
let keyResponder: E2EKeyResponder;
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
|
||||
const keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||
keyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
keyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
keyResponder.addKeyReceiver(BOB_TEST_USER_ID, keyReceiver);
|
||||
keyResponder.addKeyReceiver(TEST_USER_ID, keyReceiver);
|
||||
keyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
keyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
@@ -3127,6 +3293,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
.getCrypto()!
|
||||
.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
|
||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.needsUserApproval).toBe(false);
|
||||
});
|
||||
|
||||
it("Cross signing keys are available for a tracked user", async () => {
|
||||
@@ -3137,11 +3309,60 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// Alice is the local user and should be tracked !
|
||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
|
||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.needsUserApproval).toBe(false);
|
||||
});
|
||||
|
||||
it("Cross signing keys are not available for an unknown user", async () => {
|
||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
|
||||
expect(hasCrossSigningKeysForUser).toBe(false);
|
||||
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.needsUserApproval).toBe(false);
|
||||
});
|
||||
|
||||
newBackendOnly("An unverified user changes identity", async () => {
|
||||
// We have to be tracking Bob's keys, which means we need to share a room with him
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
...getSyncResponse([BOB_TEST_USER_ID]),
|
||||
device_lists: { changed: [BOB_TEST_USER_ID] },
|
||||
});
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(BOB_TEST_USER_ID);
|
||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||
|
||||
// Bob changes his cross-signing keys
|
||||
keyResponder.addCrossSigningData(testData.BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: "2",
|
||||
device_lists: { changed: [BOB_TEST_USER_ID] },
|
||||
});
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
await aliceClient.getCrypto()!.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
|
||||
|
||||
{
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.needsUserApproval).toBe(true);
|
||||
}
|
||||
|
||||
// Pinning the new identity should clear the needsUserApproval flag.
|
||||
await aliceClient.getCrypto()!.pinCurrentUserIdentity(BOB_TEST_USER_ID);
|
||||
{
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
expect(verificationStatus.needsUserApproval).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3278,7 +3499,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
return client;
|
||||
}
|
||||
|
||||
function mkEncryptionEvent(content: Object) {
|
||||
function mkEncryptionEvent(content: object) {
|
||||
return mkEventCustom({
|
||||
sender: persistentStoreClient.getSafeUserId(),
|
||||
type: "m.room.encryption",
|
||||
@@ -3291,7 +3512,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
*
|
||||
* @param stateEvents - Additional state events for the test room
|
||||
*/
|
||||
function getSyncResponseWithState(stateEvents: Array<Object>) {
|
||||
function getSyncResponseWithState(stateEvents: Array<object>) {
|
||||
const roomResponse = {
|
||||
state: {
|
||||
events: [
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import { createClient, MatrixClient } from "../../../src";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
/**
|
||||
* Integration tests for to-device messages functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
beforeEach(
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: testData.TEST_USER_ID,
|
||||
accessToken: "akjgkrgjsalice",
|
||||
deviceId: testData.TEST_DEVICE_ID,
|
||||
});
|
||||
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
new E2EKeyReceiver(homeserverUrl);
|
||||
const syncResponder = new SyncResponder(homeserverUrl);
|
||||
|
||||
// add bob as known user
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
|
||||
|
||||
// Silence warnings from the backup manager
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND" },
|
||||
});
|
||||
|
||||
fetchMock.get(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.get(new URL("/_matrix/client/versions/", homeserverUrl).toString(), {});
|
||||
fetchMock.post(
|
||||
new URL(
|
||||
`/_matrix/client/v3/user/${encodeURIComponent(testData.TEST_USER_ID)}/filter`,
|
||||
homeserverUrl,
|
||||
).toString(),
|
||||
{ filter_id: "fid" },
|
||||
);
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
},
|
||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||
10000,
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe("encryptToDeviceMessages", () => {
|
||||
it("returns empty batch for device that is not known", async () => {
|
||||
await aliceClient.startClient();
|
||||
|
||||
const toDeviceBatch = await aliceClient
|
||||
.getCrypto()
|
||||
?.encryptToDeviceMessages(
|
||||
"m.test.event",
|
||||
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
|
||||
{
|
||||
some: "content",
|
||||
},
|
||||
);
|
||||
|
||||
expect(toDeviceBatch).toBeDefined();
|
||||
const { batch, eventType } = toDeviceBatch!;
|
||||
expect(eventType).toBe("m.room.encrypted");
|
||||
expect(batch.length).toBe(0);
|
||||
});
|
||||
|
||||
it("returns encrypted batch for known device", async () => {
|
||||
await aliceClient.startClient();
|
||||
e2eKeyResponder.addDeviceKeys(testData.BOB_SIGNED_TEST_DEVICE_DATA);
|
||||
fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({
|
||||
one_time_keys: testData.BOB_ONE_TIME_KEYS,
|
||||
}));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const toDeviceBatch = await aliceClient
|
||||
.getCrypto()
|
||||
?.encryptToDeviceMessages(
|
||||
"m.test.event",
|
||||
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
|
||||
{
|
||||
some: "content",
|
||||
},
|
||||
);
|
||||
|
||||
expect(toDeviceBatch?.batch.length).toBe(1);
|
||||
expect(toDeviceBatch?.eventType).toBe("m.room.encrypted");
|
||||
const { deviceId, payload, userId } = toDeviceBatch!.batch[0];
|
||||
expect(deviceId).toBe(testData.BOB_TEST_DEVICE_ID);
|
||||
expect(userId).toBe(testData.BOB_TEST_USER_ID);
|
||||
expect(payload.algorithm).toBe("m.olm.v1.curve25519-aes-sha2");
|
||||
expect(payload.sender_key).toEqual(expect.any(String));
|
||||
expect(payload.ciphertext).toEqual(
|
||||
expect.objectContaining({
|
||||
[testData.BOB_SIGNED_TEST_DEVICE_DATA.keys[`curve25519:${testData.BOB_TEST_DEVICE_ID}`]]: {
|
||||
body: expect.any(String),
|
||||
type: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// for future: check that bob's device can decrypt the ciphertext?
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -95,6 +95,7 @@ beforeAll(async () => {
|
||||
|
||||
// load the rust library. This can take a few seconds on a slow GH worker.
|
||||
beforeAll(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const RustSdkCryptoJs = await require("@matrix-org/matrix-sdk-crypto-wasm");
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
}, 10000);
|
||||
|
||||
@@ -267,8 +267,9 @@ describe("MSC4108SignInWithQR", () => {
|
||||
|
||||
it("should abort if device doesn't come up by timeout", async () => {
|
||||
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
|
||||
(<Function>fn)();
|
||||
return -1;
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
});
|
||||
jest.spyOn(Date, "now").mockImplementation(() => {
|
||||
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
|
||||
@@ -319,8 +320,9 @@ describe("MSC4108SignInWithQR", () => {
|
||||
|
||||
it("should not send secrets if user cancels", async () => {
|
||||
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
|
||||
(<Function>fn)();
|
||||
return -1;
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
});
|
||||
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import { logger } from "../src/logger";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
globalThis.Olm = require("@matrix-org/olm");
|
||||
logger.log("loaded libolm");
|
||||
} catch (e) {
|
||||
|
||||
@@ -27,7 +27,7 @@ export class AccountDataAccumulator {
|
||||
* Will be updated when fetchMock intercepts calls to PUT `/_matrix/client/v3/user/:userId/account_data/`.
|
||||
* Will be used by `sendSyncResponseWithUpdatedAccountData`
|
||||
*/
|
||||
public accountDataEvents: Map<String, any> = new Map();
|
||||
public accountDataEvents: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* Intercept requests to set a particular type of account data.
|
||||
@@ -101,7 +101,7 @@ export class AccountDataAccumulator {
|
||||
})),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// Might fail with "Cannot queue more than one /sync response" if called too often.
|
||||
// It's ok if it fails here, the sync response is cumulative and will contain
|
||||
// the latest account data.
|
||||
|
||||
@@ -101,9 +101,8 @@ export const makeGeolocationPosition = ({
|
||||
}: {
|
||||
timestamp?: number;
|
||||
coords: Partial<GeolocationCoordinates>;
|
||||
}): GeolocationPosition => ({
|
||||
timestamp: timestamp ?? 1647256791840,
|
||||
coords: {
|
||||
}): GeolocationPosition => {
|
||||
const { toJSON, ...coordsJSON } = {
|
||||
accuracy: 1,
|
||||
latitude: 54.001927,
|
||||
longitude: -8.253491,
|
||||
@@ -112,5 +111,16 @@ export const makeGeolocationPosition = ({
|
||||
heading: null,
|
||||
speed: null,
|
||||
...coords,
|
||||
},
|
||||
});
|
||||
};
|
||||
const posJSON = {
|
||||
timestamp: timestamp ?? 1647256791840,
|
||||
coords: {
|
||||
toJSON: () => coordsJSON,
|
||||
...coordsJSON,
|
||||
},
|
||||
};
|
||||
return {
|
||||
toJSON: () => posJSON,
|
||||
...posJSON,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -66,6 +66,7 @@ BOB_DATA = {
|
||||
"TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"Deadmuledeadmuledeadmuledeadmule",
|
||||
|
||||
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale",
|
||||
"ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"DoYouSpeakWhaaaaaaaaaaaaaaaaaale",
|
||||
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser",
|
||||
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself",
|
||||
|
||||
@@ -208,7 +209,7 @@ def build_test_data(user_data, prefix = "") -> str:
|
||||
|
||||
backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"])
|
||||
|
||||
return f"""\
|
||||
result = f"""\
|
||||
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
|
||||
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
|
||||
export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}";
|
||||
@@ -239,7 +240,7 @@ export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_
|
||||
|
||||
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
json.dumps(build_cross_signing_keys_data(user_data), indent=4)
|
||||
json.dumps(build_cross_signing_keys_data(user_data, user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]), indent=4)
|
||||
};
|
||||
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
@@ -279,12 +280,20 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
|
||||
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
|
||||
"""
|
||||
|
||||
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
|
||||
if alt_master_key is not None:
|
||||
result += f"""
|
||||
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const {prefix}ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
json.dumps(build_cross_signing_keys_data(user_data, alt_master_key), indent=4)
|
||||
};
|
||||
"""
|
||||
|
||||
def build_cross_signing_keys_data(user_data) -> dict:
|
||||
return result
|
||||
|
||||
def build_cross_signing_keys_data(user_data, master_key_bytes) -> dict:
|
||||
"""Build the signed cross-signing-keys data for return from /keys/query"""
|
||||
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(master_key_bytes)
|
||||
b64_master_public_key = encode_base64(
|
||||
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
|
||||
@@ -449,3 +449,50 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
"master_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"master"
|
||||
]
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"self_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "eDZETBRUw9yW0WJnBZ7vxo12TW09Yb7/47qBPKZzPZzZEvs9M82dnAOtWUv00mcTdp2K9GpeFYDQJ6qLQgxaCA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_signing_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"user_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "Q1CbIXvp2BxBsu3F/eZ1ZpuR5rXIt0+FrrA/l6itskpW748xwMoIKxQRVQqs87kh7pCsWEoTy6FzIL8nV+P6BQ"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = jest.fn();
|
||||
}
|
||||
} catch (ex) {
|
||||
} catch {
|
||||
// Direct access to some non-function fields of DOM prototypes may
|
||||
// cause exceptions.
|
||||
// Overwriting will not work either in that case.
|
||||
|
||||
@@ -147,10 +147,13 @@ export class MockRTCPeerConnection {
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
const localDescriptionJSON = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: "offer",
|
||||
toJSON: function () {},
|
||||
type: "offer" as RTCSdpType,
|
||||
};
|
||||
this.localDescription = {
|
||||
toJSON: () => localDescriptionJSON,
|
||||
...localDescriptionJSON,
|
||||
};
|
||||
|
||||
this.readyToNegotiate = new Promise<void>((resolve) => {
|
||||
@@ -265,7 +268,7 @@ export class MockRTCRtpTransceiver {
|
||||
this.peerConn.needsNegotiation = true;
|
||||
}
|
||||
|
||||
public setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
|
||||
public setCodecPreferences = jest.fn<void, RTCRtpCodec[]>();
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { defer } from "../../src/utils";
|
||||
|
||||
describe("onResumedSync", () => {
|
||||
let batch: IndexedToDeviceBatch | null;
|
||||
let shouldFailSendToDevice: Boolean;
|
||||
let shouldFailSendToDevice: boolean;
|
||||
let onSendToDeviceFailure: () => void;
|
||||
let onSendToDeviceSuccess: () => void;
|
||||
let resumeSync: (newState: SyncState, oldState: SyncState) => void;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { keyFromAuthData } from "../../../src/common-crypto/key-passphrase.ts";
|
||||
|
||||
describe("key-passphrase", () => {
|
||||
describe("keyFromAuthData", () => {
|
||||
it("should throw an error if salt or iterations are missing", async () => {
|
||||
// missing salt
|
||||
expect(() => keyFromAuthData({ private_key_iterations: 5 }, "passphrase")).toThrow(
|
||||
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
|
||||
);
|
||||
|
||||
// missing iterations
|
||||
expect(() => keyFromAuthData({ private_key_salt: "salt" }, "passphrase")).toThrow(
|
||||
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
|
||||
);
|
||||
});
|
||||
|
||||
it("should derive key from auth data", async () => {
|
||||
const key = await keyFromAuthData({ private_key_salt: "salt", private_key_iterations: 5 }, "passphrase");
|
||||
expect(key).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { decodeRecoveryKey, encodeRecoveryKey } from "../../../src/crypto-api";
|
||||
|
||||
describe("recovery key", () => {
|
||||
describe("decodeRecoveryKey", () => {
|
||||
it("should thrown an incorrect length error", () => {
|
||||
const key = [0, 1];
|
||||
const encodedKey = encodeRecoveryKey(key)!;
|
||||
|
||||
expect(() => decodeRecoveryKey(encodedKey)).toThrow("Incorrect length");
|
||||
});
|
||||
|
||||
it("should thrown an incorrect parity", () => {
|
||||
const key = Array.from({ length: 32 }, (_, i) => i);
|
||||
let encodedKey = encodeRecoveryKey(key)!;
|
||||
// Mutate the encoded key to have incorrect parity
|
||||
encodedKey = encodedKey.replace("EsSz", "EsSZ");
|
||||
|
||||
expect(() => decodeRecoveryKey(encodedKey!)).toThrow("Incorrect parity");
|
||||
});
|
||||
|
||||
it("should decode a valid encoded key", () => {
|
||||
const key = Array.from({ length: 32 }, (_, i) => i);
|
||||
const encodedKey = encodeRecoveryKey(key)!;
|
||||
|
||||
expect(decodeRecoveryKey(encodedKey)).toEqual(new Uint8Array(key));
|
||||
});
|
||||
});
|
||||
});
|
||||
+113
-1
@@ -26,6 +26,7 @@ import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend";
|
||||
import * as testData from "../test-utils/test-data";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
import type { DeviceInfoMap } from "../../src/crypto/DeviceList";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -607,7 +608,7 @@ describe("Crypto", function () {
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
@@ -1245,6 +1246,117 @@ describe("Crypto", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptToDeviceMessages", () => {
|
||||
let client: TestClient;
|
||||
let ensureOlmSessionsForDevices: jest.SpiedFunction<typeof olmlib.ensureOlmSessionsForDevices>;
|
||||
let encryptMessageForDevice: jest.SpiedFunction<typeof olmlib.encryptMessageForDevice>;
|
||||
const payload = { hello: "world" };
|
||||
let encryptedPayload: object;
|
||||
let crypto: Crypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
|
||||
ensureOlmSessionsForDevices.mockResolvedValue(new Map());
|
||||
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
|
||||
encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => {
|
||||
result.plaintext = { type: 0, body: JSON.stringify(payload) };
|
||||
});
|
||||
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
|
||||
// running initCrypto should trigger a key upload
|
||||
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
|
||||
await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
|
||||
|
||||
encryptedPayload = {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } },
|
||||
};
|
||||
|
||||
crypto = client.client.getCrypto() as Crypto;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
ensureOlmSessionsForDevices.mockRestore();
|
||||
encryptMessageForDevice.mockRestore();
|
||||
await client.stop();
|
||||
});
|
||||
|
||||
it("returns encrypted batch where devices known", async () => {
|
||||
const deviceInfoMap: DeviceInfoMap = new Map([
|
||||
[
|
||||
"@bob:example.org",
|
||||
new Map([
|
||||
["bobweb", new DeviceInfo("bobweb")],
|
||||
["bobmobile", new DeviceInfo("bobmobile")],
|
||||
]),
|
||||
],
|
||||
["@carol:example.org", new Map([["caroldesktop", new DeviceInfo("caroldesktop")]])],
|
||||
]);
|
||||
jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(deviceInfoMap);
|
||||
// const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false);
|
||||
|
||||
const batch = await client.client.getCrypto()?.encryptToDeviceMessages(
|
||||
"m.test.type",
|
||||
[
|
||||
{ userId: "@bob:example.org", deviceId: "bobweb" },
|
||||
{ userId: "@bob:example.org", deviceId: "bobmobile" },
|
||||
{ userId: "@carol:example.org", deviceId: "caroldesktop" },
|
||||
{ userId: "@carol:example.org", deviceId: "carolmobile" }, // not known
|
||||
],
|
||||
payload,
|
||||
);
|
||||
expect(crypto.deviceList.downloadKeys).toHaveBeenCalledWith(
|
||||
["@bob:example.org", "@carol:example.org"],
|
||||
false,
|
||||
);
|
||||
expect(encryptMessageForDevice).toHaveBeenCalledTimes(3);
|
||||
const expectedPayload = expect.objectContaining({
|
||||
...encryptedPayload,
|
||||
"org.matrix.msgid": expect.any(String),
|
||||
"sender_key": expect.any(String),
|
||||
});
|
||||
expect(batch?.eventType).toEqual("m.room.encrypted");
|
||||
expect(batch?.batch.length).toEqual(3);
|
||||
expect(batch).toEqual({
|
||||
eventType: "m.room.encrypted",
|
||||
batch: expect.arrayContaining([
|
||||
{
|
||||
userId: "@bob:example.org",
|
||||
deviceId: "bobweb",
|
||||
payload: expectedPayload,
|
||||
},
|
||||
{
|
||||
userId: "@bob:example.org",
|
||||
deviceId: "bobmobile",
|
||||
payload: expectedPayload,
|
||||
},
|
||||
{
|
||||
userId: "@carol:example.org",
|
||||
deviceId: "caroldesktop",
|
||||
payload: expectedPayload,
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it("returns empty batch if no devices known", async () => {
|
||||
jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(new Map());
|
||||
const batch = await crypto.encryptToDeviceMessages(
|
||||
"m.test.type",
|
||||
[
|
||||
{ deviceId: "AAA", userId: "@user1:domain" },
|
||||
{ deviceId: "BBB", userId: "@user1:domain" },
|
||||
{ deviceId: "CCC", userId: "@user2:domain" },
|
||||
],
|
||||
payload,
|
||||
);
|
||||
expect(batch?.eventType).toEqual("m.room.encrypted");
|
||||
expect(batch?.batch).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkSecretStoragePrivateKey", () => {
|
||||
let client: TestClient;
|
||||
|
||||
|
||||
@@ -140,9 +140,7 @@ describe("Cross Signing", function () {
|
||||
});
|
||||
}
|
||||
|
||||
const error = new MatrixError(errorResponse);
|
||||
error.httpStatus == 401;
|
||||
throw error;
|
||||
throw new MatrixError(errorResponse, 401);
|
||||
};
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
@@ -827,7 +825,7 @@ describe("Cross Signing", function () {
|
||||
});
|
||||
|
||||
it("should offer to upgrade device verifications to cross-signing", async function () {
|
||||
let upgradeResolveFunc: Function;
|
||||
let upgradeResolveFunc: () => void;
|
||||
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
@@ -866,7 +864,7 @@ describe("Cross Signing", function () {
|
||||
// cross-signing key is signed by his Dynabook, which alice has
|
||||
// verified, and ask if the device verification should be upgraded to a
|
||||
// cross-signing verification
|
||||
let upgradePromise = new Promise((resolve) => {
|
||||
let upgradePromise = new Promise<void>((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
await resetCrossSigningKeys(alice);
|
||||
@@ -885,7 +883,7 @@ describe("Cross Signing", function () {
|
||||
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
|
||||
expect(bobTrust2.isTofu()).toBeTruthy();
|
||||
|
||||
upgradePromise = new Promise((resolve) => {
|
||||
upgradePromise = new Promise<void>((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
alice.crypto!.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
|
||||
|
||||
@@ -20,7 +20,7 @@ import { IObject } from "../../../src/crypto/olmlib";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { makeTestClients } from "./verification/util";
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
|
||||
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
|
||||
@@ -612,7 +612,7 @@ describe("Secrets", function () {
|
||||
type: "m.megolm_backup.v1",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: await encryptAES(
|
||||
key_id: await encryptAESSecretStorageItem(
|
||||
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
|
||||
secretStorageKeys.key_id,
|
||||
"m.megolm_backup.v1",
|
||||
|
||||
@@ -454,7 +454,7 @@ describe("SAS verification", function () {
|
||||
let bobSasEvent: ISasEvent | null;
|
||||
let aliceVerifier: SAS;
|
||||
let bobPromise: Promise<void>;
|
||||
let clearTestClientTimeouts: Function;
|
||||
let clearTestClientTimeouts: () => void;
|
||||
|
||||
beforeEach(async function () {
|
||||
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { encodeUnpaddedBase64Url } from "../../src";
|
||||
import { sha256 } from "../../src/digest";
|
||||
|
||||
describe("sha256", () => {
|
||||
it("should hash a string", async () => {
|
||||
const hash = await sha256("test");
|
||||
expect(encodeUnpaddedBase64Url(hash)).toBe("n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg");
|
||||
});
|
||||
|
||||
it("should hash a string with emoji", async () => {
|
||||
const hash = await sha256("test 🍱");
|
||||
expect(encodeUnpaddedBase64Url(hash)).toBe("X2aDNrrwfq3nCTOl90R9qg9ynxhHnSzsMqtrdYX-SGw");
|
||||
});
|
||||
|
||||
it("throws if webcrypto is not available", async () => {
|
||||
const oldCrypto = global.crypto;
|
||||
try {
|
||||
global.crypto = {} as any;
|
||||
await expect(sha256("test")).rejects.toThrow();
|
||||
} finally {
|
||||
global.crypto = oldCrypto;
|
||||
}
|
||||
});
|
||||
});
|
||||
+236
-3
@@ -32,7 +32,7 @@ import {
|
||||
IOpenIDCredentials,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
|
||||
import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { ICapabilities } from "../../src/embedded";
|
||||
@@ -59,8 +59,26 @@ class MockWidgetApi extends EventEmitter {
|
||||
public requestCapabilityToReceiveState = jest.fn();
|
||||
public requestCapabilityToSendToDevice = jest.fn();
|
||||
public requestCapabilityToReceiveToDevice = jest.fn();
|
||||
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
|
||||
public sendStateEvent = jest.fn();
|
||||
public sendRoomEvent = jest.fn(
|
||||
(eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
|
||||
delay === undefined && parentDelayId === undefined
|
||||
? { event_id: `$${Math.random()}` }
|
||||
: { delay_id: `id-${Math.random()}` },
|
||||
);
|
||||
public sendStateEvent = jest.fn(
|
||||
(
|
||||
eventType: string,
|
||||
stateKey: string,
|
||||
content: unknown,
|
||||
roomId?: string,
|
||||
delay?: number,
|
||||
parentDelayId?: string,
|
||||
) =>
|
||||
delay === undefined && parentDelayId === undefined
|
||||
? { event_id: `$${Math.random()}` }
|
||||
: { delay_id: `id-${Math.random()}` },
|
||||
);
|
||||
public updateDelayedEvent = jest.fn();
|
||||
public sendToDevice = jest.fn();
|
||||
public requestOpenIDConnectToken = jest.fn(() => {
|
||||
return testOIDCToken;
|
||||
@@ -125,6 +143,17 @@ describe("RoomWidgetClient", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("send handles wrong field in response", async () => {
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
widgetApi.sendRoomEvent.mockResolvedValueOnce({
|
||||
room_id: "!1:example.org",
|
||||
delay_id: `id-${Math.random}`,
|
||||
});
|
||||
await expect(
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("receives", async () => {
|
||||
const event = new MatrixEvent({
|
||||
type: "org.matrix.rageshake_request",
|
||||
@@ -160,6 +189,199 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("delayed events", () => {
|
||||
describe("when supported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = jest.fn((feature) =>
|
||||
Promise.resolve(feature === "org.matrix.msc4140"),
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("sends delayed message events", async () => {
|
||||
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||
await client._unstable_sendDelayedEvent(
|
||||
"!1:example.org",
|
||||
{ delay: 2000 },
|
||||
null,
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
);
|
||||
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
"!1:example.org",
|
||||
2000,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends child action delayed message events", async () => {
|
||||
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||
const parentDelayId = `id-${Math.random()}`;
|
||||
await client._unstable_sendDelayedEvent(
|
||||
"!1:example.org",
|
||||
{ parent_delay_id: parentDelayId },
|
||||
null,
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
);
|
||||
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
parentDelayId,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends delayed state events", async () => {
|
||||
await makeClient({
|
||||
sendDelayedEvents: true,
|
||||
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
|
||||
});
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||
await client._unstable_sendDelayedStateEvent(
|
||||
"!1:example.org",
|
||||
{ delay: 2000 },
|
||||
"org.example.foo",
|
||||
{ hello: "world" },
|
||||
"bar",
|
||||
);
|
||||
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
|
||||
"org.example.foo",
|
||||
"bar",
|
||||
{ hello: "world" },
|
||||
"!1:example.org",
|
||||
2000,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends child action delayed state events", async () => {
|
||||
await makeClient({
|
||||
sendDelayedEvents: true,
|
||||
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
|
||||
});
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||
const parentDelayId = `fg-${Math.random()}`;
|
||||
await client._unstable_sendDelayedStateEvent(
|
||||
"!1:example.org",
|
||||
{ parent_delay_id: parentDelayId },
|
||||
"org.example.foo",
|
||||
{ hello: "world" },
|
||||
"bar",
|
||||
);
|
||||
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
|
||||
"org.example.foo",
|
||||
"bar",
|
||||
{ hello: "world" },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
parentDelayId,
|
||||
);
|
||||
});
|
||||
|
||||
it("send delayed message events handles wrong field in response", async () => {
|
||||
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
widgetApi.sendRoomEvent.mockResolvedValueOnce({
|
||||
room_id: "!1:example.org",
|
||||
event_id: `$${Math.random()}`,
|
||||
});
|
||||
await expect(
|
||||
client._unstable_sendDelayedEvent(
|
||||
"!1:example.org",
|
||||
{ delay: 2000 },
|
||||
null,
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("send delayed state events handles wrong field in response", async () => {
|
||||
await makeClient({
|
||||
sendDelayedEvents: true,
|
||||
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
|
||||
});
|
||||
widgetApi.sendStateEvent.mockResolvedValueOnce({
|
||||
room_id: "!1:example.org",
|
||||
event_id: `$${Math.random()}`,
|
||||
});
|
||||
await expect(
|
||||
client._unstable_sendDelayedStateEvent(
|
||||
"!1:example.org",
|
||||
{ delay: 2000 },
|
||||
"org.example.foo",
|
||||
{ hello: "world" },
|
||||
"bar",
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("updates delayed events", async () => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
for (const action of [
|
||||
UpdateDelayedEventAction.Cancel,
|
||||
UpdateDelayedEventAction.Restart,
|
||||
UpdateDelayedEventAction.Send,
|
||||
]) {
|
||||
await client._unstable_updateDelayedEvent("id", action);
|
||||
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsupported", () => {
|
||||
it("fails to send delayed message events", async () => {
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
await expect(
|
||||
client._unstable_sendDelayedEvent(
|
||||
"!1:example.org",
|
||||
{ delay: 2000 },
|
||||
null,
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
),
|
||||
).rejects.toThrow("Server does not support");
|
||||
});
|
||||
|
||||
it("fails to send delayed state events", async () => {
|
||||
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
await expect(
|
||||
client._unstable_sendDelayedStateEvent(
|
||||
"!1:example.org",
|
||||
{ delay: 2000 },
|
||||
"org.example.foo",
|
||||
{ hello: "world" },
|
||||
"bar",
|
||||
),
|
||||
).rejects.toThrow("Server does not support");
|
||||
});
|
||||
|
||||
it("fails to update delayed state events", async () => {
|
||||
await makeClient({});
|
||||
for (const action of [
|
||||
UpdateDelayedEventAction.Cancel,
|
||||
UpdateDelayedEventAction.Restart,
|
||||
UpdateDelayedEventAction.Send,
|
||||
]) {
|
||||
await expect(client._unstable_updateDelayedEvent("id", action)).rejects.toThrow(
|
||||
"Server does not support",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("initialization", () => {
|
||||
it("requests permissions for specific message types", async () => {
|
||||
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
|
||||
@@ -211,6 +433,17 @@ describe("RoomWidgetClient", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("send handles incorrect response", async () => {
|
||||
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
widgetApi.sendStateEvent.mockResolvedValueOnce({
|
||||
room_id: "!1:example.org",
|
||||
delay_id: `id-${Math.random}`,
|
||||
});
|
||||
await expect(
|
||||
client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar"),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("receives", async () => {
|
||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
|
||||
+498
-14
@@ -57,6 +57,7 @@ import {
|
||||
Room,
|
||||
RuleId,
|
||||
TweakName,
|
||||
UpdateDelayedEventAction,
|
||||
} from "../../src";
|
||||
import { supportsMatrixCall } from "../../src/webrtc/call";
|
||||
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||
@@ -97,11 +98,11 @@ type HttpLookup = {
|
||||
method: string;
|
||||
path: string;
|
||||
prefix?: string;
|
||||
data?: Record<string, any>;
|
||||
data?: Record<string, any> | Record<string, any>[];
|
||||
error?: object;
|
||||
expectBody?: Record<string, any>;
|
||||
expectQueryParams?: QueryDict;
|
||||
thenCall?: Function;
|
||||
thenCall?: () => void;
|
||||
};
|
||||
|
||||
interface Options extends ICreateRoomOpts {
|
||||
@@ -298,7 +299,9 @@ describe("MatrixClient", function () {
|
||||
...(opts || {}),
|
||||
});
|
||||
// FIXME: We shouldn't be yanking http like this.
|
||||
client.http = (["authedRequest", "getContentUri", "request", "uploadContent"] as const).reduce((r, k) => {
|
||||
client.http = (
|
||||
["authedRequest", "getContentUri", "request", "uploadContent", "idServerRequest"] as const
|
||||
).reduce((r, k) => {
|
||||
r[k] = jest.fn();
|
||||
return r;
|
||||
}, {} as MatrixHttpApi<any>);
|
||||
@@ -400,7 +403,7 @@ describe("MatrixClient", function () {
|
||||
async function assertRequestsMade(
|
||||
responses: {
|
||||
prefix?: string;
|
||||
error?: { httpStatus: Number; errcode: string };
|
||||
error?: { httpStatus: number; errcode: string };
|
||||
data?: { event_id: string };
|
||||
}[],
|
||||
expectRejects = false,
|
||||
@@ -704,6 +707,446 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("_unstable_sendDelayedEvent", () => {
|
||||
const unstableMSC4140Prefix = `${ClientPrefix.Unstable}/org.matrix.msc4140`;
|
||||
|
||||
const roomId = "!room:example.org";
|
||||
const body = "This is the body";
|
||||
const content = { body, msgtype: MsgType.Text } satisfies RoomMessageEventContent;
|
||||
const timeoutDelayOpts = { delay: 2000 };
|
||||
const realTimeoutDelayOpts = { "org.matrix.msc4140.delay": 2000 };
|
||||
|
||||
beforeEach(() => {
|
||||
unstableFeatures["org.matrix.msc4140"] = true;
|
||||
});
|
||||
|
||||
it("throws when unsupported by server", async () => {
|
||||
unstableFeatures["org.matrix.msc4140"] = false;
|
||||
const errorMessage = "Server does not support";
|
||||
|
||||
await expect(
|
||||
client._unstable_sendDelayedEvent(
|
||||
roomId,
|
||||
timeoutDelayOpts,
|
||||
null,
|
||||
EventType.RoomMessage,
|
||||
{ ...content },
|
||||
client.makeTxnId(),
|
||||
),
|
||||
).rejects.toThrow(errorMessage);
|
||||
|
||||
await expect(
|
||||
client._unstable_sendDelayedStateEvent(roomId, timeoutDelayOpts, EventType.RoomTopic, {
|
||||
topic: "topic",
|
||||
}),
|
||||
).rejects.toThrow(errorMessage);
|
||||
|
||||
await expect(client._unstable_getDelayedEvents()).rejects.toThrow(errorMessage);
|
||||
|
||||
await expect(
|
||||
client._unstable_updateDelayedEvent("anyDelayId", UpdateDelayedEventAction.Send),
|
||||
).rejects.toThrow(errorMessage);
|
||||
});
|
||||
|
||||
it("works with null threadId", async () => {
|
||||
httpLookups = [];
|
||||
|
||||
const timeoutDelayTxnId = client.makeTxnId();
|
||||
httpLookups.push({
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
|
||||
expectQueryParams: realTimeoutDelayOpts,
|
||||
data: { delay_id: "id1" },
|
||||
expectBody: content,
|
||||
});
|
||||
|
||||
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
|
||||
roomId,
|
||||
timeoutDelayOpts,
|
||||
null,
|
||||
EventType.RoomMessage,
|
||||
{ ...content },
|
||||
timeoutDelayTxnId,
|
||||
);
|
||||
|
||||
const actionDelayTxnId = client.makeTxnId();
|
||||
httpLookups.push({
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
|
||||
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
|
||||
data: { delay_id: "id2" },
|
||||
expectBody: content,
|
||||
});
|
||||
|
||||
await client._unstable_sendDelayedEvent(
|
||||
roomId,
|
||||
{ parent_delay_id: timeoutDelayId },
|
||||
null,
|
||||
EventType.RoomMessage,
|
||||
{ ...content },
|
||||
actionDelayTxnId,
|
||||
);
|
||||
});
|
||||
|
||||
it("works with non-null threadId", async () => {
|
||||
httpLookups = [];
|
||||
const threadId = "$threadId:server";
|
||||
const expectBody = {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
event_id: threadId,
|
||||
is_falling_back: true,
|
||||
rel_type: "m.thread",
|
||||
},
|
||||
};
|
||||
|
||||
const timeoutDelayTxnId = client.makeTxnId();
|
||||
httpLookups.push({
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
|
||||
expectQueryParams: realTimeoutDelayOpts,
|
||||
data: { delay_id: "id1" },
|
||||
expectBody,
|
||||
});
|
||||
|
||||
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
|
||||
roomId,
|
||||
timeoutDelayOpts,
|
||||
threadId,
|
||||
EventType.RoomMessage,
|
||||
{ ...content },
|
||||
timeoutDelayTxnId,
|
||||
);
|
||||
|
||||
const actionDelayTxnId = client.makeTxnId();
|
||||
httpLookups.push({
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
|
||||
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
|
||||
data: { delay_id: "id2" },
|
||||
expectBody,
|
||||
});
|
||||
|
||||
await client._unstable_sendDelayedEvent(
|
||||
roomId,
|
||||
{ parent_delay_id: timeoutDelayId },
|
||||
threadId,
|
||||
EventType.RoomMessage,
|
||||
{ ...content },
|
||||
actionDelayTxnId,
|
||||
);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing", async () => {
|
||||
httpLookups = [];
|
||||
const threadId = "$threadId:server";
|
||||
const expectBody = {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: threadId,
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
};
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
mocked(store.getRoom).mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
const timeoutDelayTxnId = client.makeTxnId();
|
||||
httpLookups.push({
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
|
||||
expectQueryParams: realTimeoutDelayOpts,
|
||||
data: { delay_id: "id1" },
|
||||
expectBody,
|
||||
});
|
||||
|
||||
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
|
||||
roomId,
|
||||
timeoutDelayOpts,
|
||||
threadId,
|
||||
EventType.RoomMessage,
|
||||
{ ...content },
|
||||
timeoutDelayTxnId,
|
||||
);
|
||||
|
||||
const actionDelayTxnId = client.makeTxnId();
|
||||
httpLookups.push({
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
|
||||
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
|
||||
data: { delay_id: "id2" },
|
||||
expectBody,
|
||||
});
|
||||
|
||||
await client._unstable_sendDelayedEvent(
|
||||
roomId,
|
||||
{ parent_delay_id: timeoutDelayId },
|
||||
threadId,
|
||||
EventType.RoomMessage,
|
||||
{ ...content },
|
||||
actionDelayTxnId,
|
||||
);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
|
||||
httpLookups = [];
|
||||
const threadId = "$threadId:server";
|
||||
|
||||
const content = {
|
||||
body,
|
||||
"msgtype": MsgType.Text,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
},
|
||||
} satisfies RoomMessageEventContent;
|
||||
const expectBody = {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": false,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
};
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
mocked(store.getRoom).mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
const timeoutDelayTxnId = client.makeTxnId();
|
||||
httpLookups.push({
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
|
||||
expectQueryParams: realTimeoutDelayOpts,
|
||||
data: { delay_id: "id1" },
|
||||
expectBody,
|
||||
});
|
||||
|
||||
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
|
||||
roomId,
|
||||
timeoutDelayOpts,
|
||||
threadId,
|
||||
EventType.RoomMessage,
|
||||
{ ...content },
|
||||
timeoutDelayTxnId,
|
||||
);
|
||||
|
||||
const actionDelayTxnId = client.makeTxnId();
|
||||
httpLookups.push({
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
|
||||
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
|
||||
data: { delay_id: "id2" },
|
||||
expectBody,
|
||||
});
|
||||
|
||||
await client._unstable_sendDelayedEvent(
|
||||
roomId,
|
||||
{ parent_delay_id: timeoutDelayId },
|
||||
threadId,
|
||||
EventType.RoomMessage,
|
||||
{ ...content },
|
||||
actionDelayTxnId,
|
||||
);
|
||||
});
|
||||
|
||||
it("can send a delayed state event", async () => {
|
||||
httpLookups = [];
|
||||
const content = { topic: "The year 2000" };
|
||||
|
||||
httpLookups.push({
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/state/m.room.topic/`,
|
||||
expectQueryParams: realTimeoutDelayOpts,
|
||||
data: { delay_id: "id1" },
|
||||
expectBody: content,
|
||||
});
|
||||
|
||||
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedStateEvent(
|
||||
roomId,
|
||||
timeoutDelayOpts,
|
||||
EventType.RoomTopic,
|
||||
{ ...content },
|
||||
);
|
||||
|
||||
httpLookups.push({
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/state/m.room.topic/`,
|
||||
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
|
||||
data: { delay_id: "id2" },
|
||||
expectBody: content,
|
||||
});
|
||||
|
||||
await client._unstable_sendDelayedStateEvent(
|
||||
roomId,
|
||||
{ parent_delay_id: timeoutDelayId },
|
||||
EventType.RoomTopic,
|
||||
{ ...content },
|
||||
);
|
||||
});
|
||||
|
||||
it("can look up delayed events", async () => {
|
||||
httpLookups = [
|
||||
{
|
||||
method: "GET",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: "/delayed_events",
|
||||
data: [],
|
||||
},
|
||||
];
|
||||
|
||||
await client._unstable_getDelayedEvents();
|
||||
});
|
||||
|
||||
it("can update delayed events", async () => {
|
||||
const delayId = "id";
|
||||
const action = UpdateDelayedEventAction.Restart;
|
||||
httpLookups = [
|
||||
{
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}`,
|
||||
data: {
|
||||
action,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await client._unstable_updateDelayedEvent(delayId, action);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extended profiles", () => {
|
||||
const unstableMSC4133Prefix = `${ClientPrefix.Unstable}/uk.tcpip.msc4133`;
|
||||
const userId = "@profile_user:example.org";
|
||||
|
||||
beforeEach(() => {
|
||||
unstableFeatures["uk.tcpip.msc4133"] = true;
|
||||
});
|
||||
|
||||
it("throws when unsupported by server", async () => {
|
||||
unstableFeatures["uk.tcpip.msc4133"] = false;
|
||||
const errorMessage = "Server does not support extended profiles";
|
||||
|
||||
await expect(client.doesServerSupportExtendedProfiles()).resolves.toEqual(false);
|
||||
|
||||
await expect(client.getExtendedProfile(userId)).rejects.toThrow(errorMessage);
|
||||
await expect(client.getExtendedProfileProperty(userId, "test_key")).rejects.toThrow(errorMessage);
|
||||
await expect(client.setExtendedProfileProperty("test_key", "foo")).rejects.toThrow(errorMessage);
|
||||
await expect(client.deleteExtendedProfileProperty("test_key")).rejects.toThrow(errorMessage);
|
||||
await expect(client.patchExtendedProfile({ test_key: "foo" })).rejects.toThrow(errorMessage);
|
||||
await expect(client.setExtendedProfile({ test_key: "foo" })).rejects.toThrow(errorMessage);
|
||||
});
|
||||
|
||||
it("can fetch a extended user profile", async () => {
|
||||
const testProfile = {
|
||||
test_key: "foo",
|
||||
};
|
||||
httpLookups = [
|
||||
{
|
||||
method: "GET",
|
||||
prefix: unstableMSC4133Prefix,
|
||||
path: "/profile/" + encodeURIComponent(userId),
|
||||
data: testProfile,
|
||||
},
|
||||
];
|
||||
await expect(client.getExtendedProfile(userId)).resolves.toEqual(testProfile);
|
||||
expect(httpLookups).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("can fetch a property from a extended user profile", async () => {
|
||||
const testProfile = {
|
||||
test_key: "foo",
|
||||
};
|
||||
httpLookups = [
|
||||
{
|
||||
method: "GET",
|
||||
prefix: unstableMSC4133Prefix,
|
||||
path: "/profile/" + encodeURIComponent(userId) + "/test_key",
|
||||
data: testProfile,
|
||||
},
|
||||
];
|
||||
await expect(client.getExtendedProfileProperty(userId, "test_key")).resolves.toEqual("foo");
|
||||
expect(httpLookups).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("can set a property in our extended profile", async () => {
|
||||
httpLookups = [
|
||||
{
|
||||
method: "PUT",
|
||||
prefix: unstableMSC4133Prefix,
|
||||
path: "/profile/" + encodeURIComponent(client.credentials.userId!) + "/test_key",
|
||||
expectBody: {
|
||||
test_key: "foo",
|
||||
},
|
||||
},
|
||||
];
|
||||
await expect(client.setExtendedProfileProperty("test_key", "foo")).resolves.toEqual(undefined);
|
||||
expect(httpLookups).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("can delete a property in our extended profile", async () => {
|
||||
httpLookups = [
|
||||
{
|
||||
method: "DELETE",
|
||||
prefix: unstableMSC4133Prefix,
|
||||
path: "/profile/" + encodeURIComponent(client.credentials.userId!) + "/test_key",
|
||||
},
|
||||
];
|
||||
await expect(client.deleteExtendedProfileProperty("test_key")).resolves.toEqual(undefined);
|
||||
expect(httpLookups).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("can patch our extended profile", async () => {
|
||||
const testProfile = {
|
||||
test_key: "foo",
|
||||
};
|
||||
const patchedProfile = {
|
||||
existing: "key",
|
||||
test_key: "foo",
|
||||
};
|
||||
httpLookups = [
|
||||
{
|
||||
method: "PATCH",
|
||||
prefix: unstableMSC4133Prefix,
|
||||
path: "/profile/" + encodeURIComponent(client.credentials.userId!),
|
||||
data: patchedProfile,
|
||||
expectBody: testProfile,
|
||||
},
|
||||
];
|
||||
await expect(client.patchExtendedProfile(testProfile)).resolves.toEqual(patchedProfile);
|
||||
});
|
||||
|
||||
it("can replace our extended profile", async () => {
|
||||
const testProfile = {
|
||||
test_key: "foo",
|
||||
};
|
||||
httpLookups = [
|
||||
{
|
||||
method: "PUT",
|
||||
prefix: unstableMSC4133Prefix,
|
||||
path: "/profile/" + encodeURIComponent(client.credentials.userId!),
|
||||
data: testProfile,
|
||||
expectBody: testProfile,
|
||||
},
|
||||
];
|
||||
await expect(client.setExtendedProfile(testProfile)).resolves.toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create (unstable) file trees", async () => {
|
||||
const userId = "@test:example.org";
|
||||
const roomId = "!room:example.org";
|
||||
@@ -963,7 +1406,7 @@ describe("MatrixClient", function () {
|
||||
const filter = new Filter(client.credentials.userId);
|
||||
|
||||
const filterId = await client.getOrCreateFilter(filterName, filter);
|
||||
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
|
||||
expect(filterId).toEqual(!Array.isArray(FILTER_RESPONSE.data) && FILTER_RESPONSE.data?.filter_id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1072,7 +1515,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("emitted sync events", function () {
|
||||
function syncChecker(expectedStates: [string, string | null][], done: Function) {
|
||||
function syncChecker(expectedStates: [string, string | null][], done: () => void) {
|
||||
return function syncListener(state: SyncState, old: SyncState | null) {
|
||||
const expected = expectedStates.shift();
|
||||
logger.log("'sync' curr=%s old=%s EXPECT=%s", state, old, expected);
|
||||
@@ -1094,7 +1537,7 @@ describe("MatrixClient", function () {
|
||||
it("should transition null -> PREPARED after the first /sync", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1111,7 +1554,7 @@ describe("MatrixClient", function () {
|
||||
error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
expectedStates.push(["ERROR", null]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1151,7 +1594,7 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1162,7 +1605,7 @@ describe("MatrixClient", function () {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1187,7 +1630,7 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1206,7 +1649,7 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1221,7 +1664,7 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -1252,7 +1695,7 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["ERROR", "ERROR"]);
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
const didSyncPromise = new Promise<void>((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -3035,4 +3478,45 @@ describe("MatrixClient", function () {
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("identityHashedLookup", () => {
|
||||
it("should return hashed lookup results", async () => {
|
||||
const ID_ACCESS_TOKEN = "hello_id_server_please_let_me_make_a_request";
|
||||
|
||||
client.http.idServerRequest = jest.fn().mockImplementation((method, path, params) => {
|
||||
if (method === "GET" && path === "/hash_details") {
|
||||
return { algorithms: ["sha256"], lookup_pepper: "carrot" };
|
||||
} else if (method === "POST" && path === "/lookup") {
|
||||
return {
|
||||
mappings: {
|
||||
"WHA-MgrrsZACDI9F8OaVagpiyiV2sjZylGHJteT4OMU": "@bob:homeserver.dummy",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error("Test impl doesn't know about this request");
|
||||
});
|
||||
|
||||
const lookupResult = await client.identityHashedLookup([["bob@email.dummy", "email"]], ID_ACCESS_TOKEN);
|
||||
|
||||
expect(client.http.idServerRequest).toHaveBeenCalledWith(
|
||||
"GET",
|
||||
"/hash_details",
|
||||
undefined,
|
||||
"/_matrix/identity/v2",
|
||||
ID_ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
expect(client.http.idServerRequest).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
"/lookup",
|
||||
{ pepper: "carrot", algorithm: "sha256", addresses: ["WHA-MgrrsZACDI9F8OaVagpiyiV2sjZylGHJteT4OMU"] },
|
||||
"/_matrix/identity/v2",
|
||||
ID_ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
expect(lookupResult).toHaveLength(1);
|
||||
expect(lookupResult[0]).toEqual({ address: "bob@email.dummy", mxid: "@bob:homeserver.dummy" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,20 +98,6 @@ describe("CallMembership", () => {
|
||||
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
|
||||
});
|
||||
|
||||
it("considers memberships unexpired if local age low enough", () => {
|
||||
const fakeEvent = makeMockEvent(1000);
|
||||
fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000);
|
||||
const membership = new CallMembership(fakeEvent, membershipTemplate);
|
||||
expect(membership.isExpired()).toEqual(false);
|
||||
});
|
||||
|
||||
it("considers memberships expired when local age large", () => {
|
||||
const fakeEvent = makeMockEvent(1000);
|
||||
fakeEvent.localTimestamp = Date.now() - 6000;
|
||||
const membership = new CallMembership(fakeEvent, membershipTemplate);
|
||||
expect(membership.isExpired()).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns preferred foci", () => {
|
||||
const fakeEvent = makeMockEvent();
|
||||
const mockFocus = { type: "this_is_a_mock_focus" };
|
||||
@@ -198,13 +184,6 @@ describe("CallMembership", () => {
|
||||
beforeEach(() => {
|
||||
// server origin timestamp for this event is 1000
|
||||
fakeEvent = makeMockEvent(1000);
|
||||
// our clock would have been at 2000 at the creation time (our clock at event receive time - age)
|
||||
// (ie. the local clock is 1 second ahead of the servers' clocks)
|
||||
fakeEvent.localTimestamp = 2000;
|
||||
|
||||
// for simplicity's sake, we say that the event's age is zero
|
||||
fakeEvent.getLocalAge = jest.fn().mockReturnValue(0);
|
||||
|
||||
membership = new CallMembership(fakeEvent!, membershipTemplate);
|
||||
|
||||
jest.useFakeTimers();
|
||||
@@ -215,6 +194,13 @@ describe("CallMembership", () => {
|
||||
});
|
||||
|
||||
it("converts expiry time into local clock", () => {
|
||||
// our clock would have been at 2000 at the creation time (our clock at event receive time - age)
|
||||
// (ie. the local clock is 1 second ahead of the servers' clocks)
|
||||
fakeEvent.localTimestamp = 2000;
|
||||
|
||||
// for simplicity's sake, we say that the event's age is zero
|
||||
fakeEvent.getLocalAge = jest.fn().mockReturnValue(0);
|
||||
|
||||
// for sanity's sake, make sure the server-relative expiry time is what we expect
|
||||
expect(membership.getAbsoluteExpiry()).toEqual(6000);
|
||||
// therefore the expiry time converted to our clock should be 1 second later
|
||||
@@ -223,7 +209,8 @@ describe("CallMembership", () => {
|
||||
|
||||
it("calculates time until expiry", () => {
|
||||
jest.setSystemTime(2000);
|
||||
expect(membership.getMsUntilExpiry()).toEqual(5000);
|
||||
// should be using absolute expiry time
|
||||
expect(membership.getMsUntilExpiry()).toEqual(4000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -102,6 +102,7 @@ describe("MatrixRTCSessionManager", () => {
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
||||
getRoomId: jest.fn().mockReturnValue("!room:id"),
|
||||
isDecryptionFailure: jest.fn().mockReturnValue(false),
|
||||
sender: {
|
||||
userId: "@mock:user.example",
|
||||
},
|
||||
@@ -110,4 +111,93 @@ describe("MatrixRTCSessionManager", () => {
|
||||
await new Promise(process.nextTick);
|
||||
expect(onCallEncryptionMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("event decryption", () => {
|
||||
it("Retries decryption and processes success", async () => {
|
||||
try {
|
||||
jest.useFakeTimers();
|
||||
const room1 = makeMockRoom([membershipTemplate]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
const onCallEncryptionMock = jest.fn();
|
||||
client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock;
|
||||
let isDecryptionFailure = true;
|
||||
client.decryptEventIfNeeded = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(Promise.resolve())
|
||||
.mockImplementation(() => {
|
||||
isDecryptionFailure = false;
|
||||
return Promise.resolve();
|
||||
});
|
||||
const timelineEvent = {
|
||||
getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix),
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
||||
getRoomId: jest.fn().mockReturnValue("!room:id"),
|
||||
isDecryptionFailure: jest.fn().mockImplementation(() => isDecryptionFailure),
|
||||
getId: jest.fn().mockReturnValue("event_id"),
|
||||
sender: {
|
||||
userId: "@mock:user.example",
|
||||
},
|
||||
} as unknown as MatrixEvent;
|
||||
client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
|
||||
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(1);
|
||||
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
// should retry after one second:
|
||||
await jest.advanceTimersByTimeAsync(1500);
|
||||
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
||||
expect(onCallEncryptionMock).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("Retries decryption and processes failure", async () => {
|
||||
try {
|
||||
jest.useFakeTimers();
|
||||
const room1 = makeMockRoom([membershipTemplate]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
const onCallEncryptionMock = jest.fn();
|
||||
client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock;
|
||||
client.decryptEventIfNeeded = jest.fn().mockReturnValue(Promise.resolve());
|
||||
const timelineEvent = {
|
||||
getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix),
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
||||
getRoomId: jest.fn().mockReturnValue("!room:id"),
|
||||
isDecryptionFailure: jest.fn().mockReturnValue(true), // always fail
|
||||
getId: jest.fn().mockReturnValue("event_id"),
|
||||
sender: {
|
||||
userId: "@mock:user.example",
|
||||
},
|
||||
} as unknown as MatrixEvent;
|
||||
client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
|
||||
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(1);
|
||||
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
// should retry after one second:
|
||||
await jest.advanceTimersByTimeAsync(1500);
|
||||
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
||||
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
// doesn't retry again:
|
||||
await jest.advanceTimersByTimeAsync(1500);
|
||||
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
||||
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,10 +20,10 @@ import { randomString } from "../../../src/randomstring";
|
||||
|
||||
type MembershipData = CallMembershipData[] | SessionMembershipData;
|
||||
|
||||
export function makeMockRoom(membershipData: MembershipData, localAge: number | null = null): Room {
|
||||
export function makeMockRoom(membershipData: MembershipData): Room {
|
||||
const roomId = randomString(8);
|
||||
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
|
||||
const roomState = makeMockRoomState(membershipData, roomId, localAge);
|
||||
const roomState = makeMockRoomState(membershipData, roomId);
|
||||
return {
|
||||
roomId: roomId,
|
||||
hasMembershipState: jest.fn().mockReturnValue(true),
|
||||
@@ -34,8 +34,8 @@ export function makeMockRoom(membershipData: MembershipData, localAge: number |
|
||||
} as unknown as Room;
|
||||
}
|
||||
|
||||
export function makeMockRoomState(membershipData: MembershipData, roomId: string, localAge: number | null = null) {
|
||||
const event = mockRTCEvent(membershipData, roomId, localAge);
|
||||
export function makeMockRoomState(membershipData: MembershipData, roomId: string) {
|
||||
const event = mockRTCEvent(membershipData, roomId);
|
||||
return {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
@@ -57,7 +57,7 @@ export function makeMockRoomState(membershipData: MembershipData, roomId: string
|
||||
};
|
||||
}
|
||||
|
||||
export function mockRTCEvent(membershipData: MembershipData, roomId: string, localAge: number | null): MatrixEvent {
|
||||
export function mockRTCEvent(membershipData: MembershipData, roomId: string): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getContent: jest.fn().mockReturnValue(
|
||||
@@ -68,11 +68,11 @@ export function mockRTCEvent(membershipData: MembershipData, roomId: string, loc
|
||||
},
|
||||
),
|
||||
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
||||
getTs: jest.fn().mockReturnValue(1000),
|
||||
localTimestamp: Date.now() - (localAge ?? 10),
|
||||
getTs: jest.fn().mockReturnValue(Date.now()),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
sender: {
|
||||
userId: "@mock:user.example",
|
||||
},
|
||||
isDecryptionFailure: jest.fn().mockReturnValue(false),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = fn;
|
||||
await tree.invite(target, false, false);
|
||||
await tree.invite(target, false);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = fn;
|
||||
await tree.invite(target, false, false);
|
||||
await tree.invite(target, false);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -133,7 +133,7 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
client.invite = fn;
|
||||
|
||||
await expect(tree.invite(target, false, false)).rejects.toThrow("MatrixError: Sample Failure");
|
||||
await expect(tree.invite(target, false)).rejects.toThrow("MatrixError: Sample Failure");
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -155,61 +155,10 @@ describe("MSC3089TreeSpace", () => {
|
||||
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
|
||||
];
|
||||
|
||||
await tree.invite(target, true, false);
|
||||
await tree.invite(target, true);
|
||||
expect(fn).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("should share keys with invitees", async () => {
|
||||
const target = targetUser;
|
||||
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userIds).toMatchObject([target]);
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
|
||||
client.sendSharedHistoryKeys = sendKeysFn;
|
||||
|
||||
// Mock the history check as best as possible
|
||||
const historyVis = "shared";
|
||||
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
|
||||
// We're not expecting a super rigid test: the function that calls this internally isn't
|
||||
// really being tested here.
|
||||
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
|
||||
expect(stateKey).toEqual("");
|
||||
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
|
||||
});
|
||||
room.currentState.getStateEvents = historyFn;
|
||||
|
||||
// Note: inverse test is implicit from other tests, which disable the call stack of this
|
||||
// test in order to pass.
|
||||
await tree.invite(target, false, true);
|
||||
expect(sendKeysFn).toHaveBeenCalledTimes(1);
|
||||
expect(historyFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should not share keys with invitees if inappropriate history visibility", async () => {
|
||||
const target = targetUser;
|
||||
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userIds).toMatchObject([target]);
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
|
||||
client.sendSharedHistoryKeys = sendKeysFn;
|
||||
|
||||
const historyVis = "joined"; // NOTE: Changed.
|
||||
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
|
||||
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
|
||||
expect(stateKey).toEqual("");
|
||||
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
|
||||
});
|
||||
room.currentState.getStateEvents = historyFn;
|
||||
|
||||
await tree.invite(target, false, true);
|
||||
expect(sendKeysFn).toHaveBeenCalledTimes(0);
|
||||
expect(historyFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
|
||||
makePowerLevels(pls);
|
||||
const fn = jest
|
||||
|
||||
@@ -412,7 +412,12 @@ describe("MatrixEvent", () => {
|
||||
const crypto = {
|
||||
decryptEvent: jest
|
||||
.fn()
|
||||
.mockRejectedValue("DecryptionError: The sender has disabled encrypting to unverified devices."),
|
||||
.mockRejectedValue(
|
||||
new DecryptionError(
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
"The sender has disabled encrypting to unverified devices.",
|
||||
),
|
||||
),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
@@ -89,11 +89,8 @@ describe("oidc authorization", () => {
|
||||
|
||||
describe("generateAuthorizationUrl()", () => {
|
||||
it("should generate url with correct parameters", async () => {
|
||||
// test the no crypto case here
|
||||
// @ts-ignore mocking
|
||||
globalThis.crypto.subtle = undefined;
|
||||
|
||||
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
|
||||
authorizationParams.codeVerifier = "test-code-verifier";
|
||||
const authUrl = new URL(
|
||||
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
|
||||
);
|
||||
@@ -105,6 +102,18 @@ describe("oidc authorization", () => {
|
||||
expect(authUrl.searchParams.get("scope")).toEqual(authorizationParams.scope);
|
||||
expect(authUrl.searchParams.get("state")).toEqual(authorizationParams.state);
|
||||
expect(authUrl.searchParams.get("nonce")).toEqual(authorizationParams.nonce);
|
||||
expect(authUrl.searchParams.get("code_challenge")).toEqual("0FLIKahrX7kqxncwhV5WD82lu_wi5GA8FsRSLubaOpU");
|
||||
});
|
||||
|
||||
it("should log a warning if crypto is not available", async () => {
|
||||
// test the no crypto case here
|
||||
// @ts-ignore mocking
|
||||
globalThis.crypto.subtle = undefined;
|
||||
|
||||
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
|
||||
const authUrl = new URL(
|
||||
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
|
||||
);
|
||||
|
||||
// crypto not available, plain text code_challenge is used
|
||||
expect(authUrl.searchParams.get("code_challenge")).toEqual(authorizationParams.codeVerifier);
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "../../olm-loader";
|
||||
import { LegacyRendezvousFailureReason as RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
|
||||
import { decodeBase64 } from "../../../src/base64";
|
||||
import { DummyTransport } from "./DummyTransport";
|
||||
|
||||
function makeTransport(name: string) {
|
||||
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "dummy" });
|
||||
}
|
||||
|
||||
describe("ECDHv2", function () {
|
||||
beforeAll(async function () {
|
||||
await global.Olm.init();
|
||||
});
|
||||
|
||||
describe("with crypto", () => {
|
||||
it("initiator wants to sign in", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
const message = { key: "xxx" };
|
||||
await alice.send(message);
|
||||
const bobReceive = await bob.receive();
|
||||
expect(bobReceive).toEqual(message);
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("initiator wants to reciprocate", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
const message = { key: "xxx" };
|
||||
await bob.send(message);
|
||||
const aliceReceive = await alice.receive();
|
||||
expect(aliceReceive).toEqual(message);
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("double connect", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
await expect(alice.connect()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("closed", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
alice.close();
|
||||
|
||||
await expect(alice.connect()).rejects.toThrow();
|
||||
await expect(alice.send({})).rejects.toThrow();
|
||||
await expect(alice.receive()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("require ciphertext", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
|
||||
expect(aliceChecksum).toEqual(bobChecksum);
|
||||
|
||||
// send a message without encryption
|
||||
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
await expect(bob.receive()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("ciphertext before set up", async function () {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob");
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
|
||||
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await expect(alice.receive()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,712 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import "../../olm-loader";
|
||||
import {
|
||||
MSC3906Rendezvous,
|
||||
RendezvousCode,
|
||||
LegacyRendezvousFailureReason as RendezvousFailureReason,
|
||||
RendezvousIntent,
|
||||
} from "../../../src/rendezvous";
|
||||
import {
|
||||
ECDHv2RendezvousCode as ECDHRendezvousCode,
|
||||
MSC3903ECDHPayload,
|
||||
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
|
||||
} from "../../../src/rendezvous/channels";
|
||||
import { Device, MatrixClient } from "../../../src";
|
||||
import {
|
||||
MSC3886SimpleHttpRendezvousTransport,
|
||||
MSC3886SimpleHttpRendezvousTransportDetails,
|
||||
} from "../../../src/rendezvous/transports";
|
||||
import { DummyTransport } from "./DummyTransport";
|
||||
import { decodeBase64 } from "../../../src/base64";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { CrossSigningKey, OwnDeviceKeys } from "../../../src/crypto-api";
|
||||
|
||||
type UserID = string;
|
||||
type DeviceID = string;
|
||||
type Fingerprint = string;
|
||||
type SimpleDeviceMap = Record<UserID, Record<DeviceID, Fingerprint>>;
|
||||
|
||||
function mockDevice(userId: UserID, deviceId: DeviceID, fingerprint: Fingerprint): Device {
|
||||
return {
|
||||
deviceId,
|
||||
userId,
|
||||
getFingerprint: () => fingerprint,
|
||||
} as unknown as Device;
|
||||
}
|
||||
|
||||
function mockDeviceMap(
|
||||
userId: UserID,
|
||||
deviceId: DeviceID,
|
||||
deviceKey?: Fingerprint,
|
||||
otherDevices: SimpleDeviceMap = {},
|
||||
): Map<string, Map<string, Device>> {
|
||||
const deviceMap: Map<string, Map<string, Device>> = new Map();
|
||||
|
||||
const myDevices: Map<string, Device> = new Map();
|
||||
if (deviceKey) {
|
||||
myDevices.set(deviceId, mockDevice(userId, deviceId, deviceKey));
|
||||
}
|
||||
deviceMap.set(userId, myDevices);
|
||||
|
||||
for (const u in otherDevices) {
|
||||
let userDevices = deviceMap.get(u);
|
||||
if (!userDevices) {
|
||||
userDevices = new Map();
|
||||
deviceMap.set(u, userDevices);
|
||||
}
|
||||
for (const d in otherDevices[u]) {
|
||||
userDevices.set(d, mockDevice(u, d, otherDevices[u][d]));
|
||||
}
|
||||
}
|
||||
|
||||
return deviceMap;
|
||||
}
|
||||
|
||||
function makeMockClient(opts: {
|
||||
userId: UserID;
|
||||
deviceId: DeviceID;
|
||||
deviceKey?: Fingerprint;
|
||||
getLoginTokenEnabled: boolean;
|
||||
msc3882r0Only: boolean;
|
||||
msc3886Enabled: boolean;
|
||||
devices?: SimpleDeviceMap;
|
||||
verificationFunction?: (
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
verified: boolean,
|
||||
blocked: boolean,
|
||||
known: boolean,
|
||||
) => void;
|
||||
crossSigningIds?: Partial<Record<CrossSigningKey, string>>;
|
||||
}): [MatrixClient, Map<string, Map<string, Device>>] {
|
||||
const deviceMap = mockDeviceMap(opts.userId, opts.deviceId, opts.deviceKey, opts.devices);
|
||||
return [
|
||||
{
|
||||
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
|
||||
if (feature === "org.matrix.msc3886") {
|
||||
return opts.msc3886Enabled;
|
||||
} else if (feature === "org.matrix.msc3882") {
|
||||
return opts.getLoginTokenEnabled;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
getVersions() {
|
||||
return {
|
||||
unstable_features: {
|
||||
"org.matrix.msc3882": opts.getLoginTokenEnabled,
|
||||
"org.matrix.msc3886": opts.msc3886Enabled,
|
||||
},
|
||||
};
|
||||
},
|
||||
getCachedCapabilities() {
|
||||
return opts.msc3882r0Only
|
||||
? {}
|
||||
: {
|
||||
capabilities: {
|
||||
"m.get_login_token": {
|
||||
enabled: opts.getLoginTokenEnabled,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getSafeUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getDeviceId() {
|
||||
return opts.deviceId;
|
||||
},
|
||||
baseUrl: "https://example.com",
|
||||
getCrypto() {
|
||||
return {
|
||||
getUserDeviceInfo(
|
||||
[userId]: string[],
|
||||
downloadUncached?: boolean,
|
||||
): Promise<Map<string, Map<string, Device>>> {
|
||||
return Promise.resolve(deviceMap);
|
||||
},
|
||||
getCrossSigningKeyId(key: CrossSigningKey): string | null {
|
||||
return opts.crossSigningIds?.[key] ?? null;
|
||||
},
|
||||
setDeviceVerified(userId: string, deviceId: string, verified: boolean): Promise<void> {
|
||||
return Promise.resolve();
|
||||
},
|
||||
crossSignDevice(deviceId: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getOwnDeviceKeys(): Promise<OwnDeviceKeys> {
|
||||
return Promise.resolve({
|
||||
ed25519: opts.deviceKey!,
|
||||
curve25519: "aaaa",
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
} as unknown as MatrixClient,
|
||||
deviceMap,
|
||||
];
|
||||
}
|
||||
|
||||
function makeTransport(name: string, uri = "https://test.rz/123456") {
|
||||
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "http.v1", uri });
|
||||
}
|
||||
|
||||
describe("Rendezvous", function () {
|
||||
beforeAll(async function () {
|
||||
await global.Olm.init();
|
||||
});
|
||||
|
||||
let httpBackend: MockHttpBackend;
|
||||
let fetchFn: typeof global.fetch;
|
||||
let transports: DummyTransport<any, MSC3903ECDHPayload>[];
|
||||
const userId: UserID = "@user:example.com";
|
||||
|
||||
beforeEach(function () {
|
||||
httpBackend = new MockHttpBackend();
|
||||
fetchFn = httpBackend.fetchFn as typeof global.fetch;
|
||||
transports = [];
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
transports.forEach((x) => x.cleanup());
|
||||
});
|
||||
|
||||
it("generate and cancel", async function () {
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
msc3886Enabled: false,
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: true,
|
||||
});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
const aliceTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client: alice,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
|
||||
expect(aliceRz.code).toBeUndefined();
|
||||
|
||||
const codePromise = aliceRz.generateCode();
|
||||
await httpBackend.flush("");
|
||||
|
||||
await aliceRz.generateCode();
|
||||
|
||||
expect(typeof aliceRz.code).toBe("string");
|
||||
|
||||
await codePromise;
|
||||
|
||||
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
|
||||
|
||||
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
|
||||
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256");
|
||||
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
|
||||
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual(
|
||||
"https://fallbackserver/rz/123",
|
||||
);
|
||||
|
||||
httpBackend.when("DELETE", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
|
||||
const cancelPromise = aliceRz.cancel(RendezvousFailureReason.UserDeclined);
|
||||
await httpBackend.flush("");
|
||||
await expect(cancelPromise).resolves.toBeUndefined();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
|
||||
await aliceRz.close();
|
||||
});
|
||||
|
||||
async function testNoProtocols({
|
||||
getLoginTokenEnabled,
|
||||
msc3882r0Only,
|
||||
}: {
|
||||
getLoginTokenEnabled: boolean;
|
||||
msc3882r0Only: boolean;
|
||||
}) {
|
||||
const aliceTransport = makeTransport("Alice");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
msc3886Enabled: false,
|
||||
getLoginTokenEnabled,
|
||||
msc3882r0Only,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.finish",
|
||||
outcome: "unsupported",
|
||||
});
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
}
|
||||
|
||||
it("no protocols - r0", async function () {
|
||||
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: true });
|
||||
});
|
||||
|
||||
it("no protocols - stable", async function () {
|
||||
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: false });
|
||||
});
|
||||
|
||||
it("new device declines protocol with outcome unsupported", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.progress",
|
||||
protocols: ["org.matrix.msc3906.login_token"],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: "m.login.finish", outcome: "unsupported" });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
});
|
||||
|
||||
it("new device requests an invalid protocol", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.progress",
|
||||
protocols: ["org.matrix.msc3906.login_token"],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: "m.login.progress", protocol: "bad protocol" });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
});
|
||||
|
||||
it("decline on existing device", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const [alice] = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.progress",
|
||||
protocols: ["org.matrix.msc3906.login_token"],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
await aliceRz.declineLoginOnExistingDevice();
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: "m.login.finish", outcome: "declined" });
|
||||
});
|
||||
|
||||
it("approve on existing device + no verification", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const [alice] = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.progress",
|
||||
protocols: ["org.matrix.msc3906.login_token"],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
|
||||
|
||||
const bobCompleteProm = (async () => {
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: "m.login.progress", login_token: "token", homeserver: alice.baseUrl });
|
||||
await bobEcdh.send({ type: "m.login.finish", outcome: "success" });
|
||||
})();
|
||||
|
||||
await confirmProm;
|
||||
await bobCompleteProm;
|
||||
});
|
||||
|
||||
async function completeLogin(devices: SimpleDeviceMap) {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
aliceTransport.otherParty = bobTransport;
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const aliceVerification = jest.fn();
|
||||
const [alice, deviceMap] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
msc3886Enabled: false,
|
||||
devices,
|
||||
deviceKey: "aaaa",
|
||||
verificationFunction: aliceVerification,
|
||||
crossSigningIds: {
|
||||
master: "mmmmm",
|
||||
},
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
const aliceStartProm = aliceRz.startAfterShowingCode();
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
);
|
||||
|
||||
const bobStartPromise = (async () => {
|
||||
const bobChecksum = await bobEcdh.connect();
|
||||
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
|
||||
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
|
||||
|
||||
// wait for protocols
|
||||
logger.info("Bob waiting for protocols");
|
||||
const protocols = await bobEcdh.receive();
|
||||
|
||||
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
|
||||
|
||||
expect(protocols).toEqual({
|
||||
type: "m.login.progress",
|
||||
protocols: ["org.matrix.msc3906.login_token"],
|
||||
});
|
||||
|
||||
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
|
||||
})();
|
||||
|
||||
await aliceStartProm;
|
||||
await bobStartPromise;
|
||||
|
||||
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
|
||||
|
||||
const bobLoginProm = (async () => {
|
||||
const loginToken = await bobEcdh.receive();
|
||||
expect(loginToken).toEqual({ type: "m.login.progress", login_token: "token", homeserver: alice.baseUrl });
|
||||
await bobEcdh.send({ type: "m.login.finish", outcome: "success", device_id: "BOB", device_key: "bbbb" });
|
||||
})();
|
||||
|
||||
expect(await confirmProm).toEqual("BOB");
|
||||
await bobLoginProm;
|
||||
|
||||
return {
|
||||
aliceTransport,
|
||||
aliceEcdh,
|
||||
aliceRz,
|
||||
bobTransport,
|
||||
bobEcdh,
|
||||
deviceMap,
|
||||
};
|
||||
}
|
||||
|
||||
it("approve on existing device + verification", async function () {
|
||||
const { bobEcdh, aliceRz } = await completeLogin({
|
||||
[userId]: {
|
||||
BOB: "bbbb",
|
||||
},
|
||||
});
|
||||
const verifyProm = aliceRz.verifyNewDeviceOnExistingDevice();
|
||||
|
||||
const bobVerifyProm = (async () => {
|
||||
const verified = await bobEcdh.receive();
|
||||
expect(verified).toEqual({
|
||||
type: "m.login.finish",
|
||||
outcome: "verified",
|
||||
verifying_device_id: "ALICE",
|
||||
verifying_device_key: "aaaa",
|
||||
master_key: "mmmmm",
|
||||
});
|
||||
})();
|
||||
|
||||
await verifyProm;
|
||||
await bobVerifyProm;
|
||||
});
|
||||
|
||||
it("device not online within timeout", async function () {
|
||||
const { aliceRz } = await completeLogin({});
|
||||
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("device appears online within timeout", async function () {
|
||||
const devices: SimpleDeviceMap = {};
|
||||
const { aliceRz, deviceMap } = await completeLogin(devices);
|
||||
// device appears before the timeout
|
||||
setTimeout(() => {
|
||||
deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb"));
|
||||
}, 1000);
|
||||
await aliceRz.verifyNewDeviceOnExistingDevice(2000);
|
||||
});
|
||||
|
||||
it("device appears online after timeout", async function () {
|
||||
const devices: SimpleDeviceMap = {};
|
||||
const { aliceRz, deviceMap } = await completeLogin(devices);
|
||||
// device appears after the timeout
|
||||
setTimeout(() => {
|
||||
deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb"));
|
||||
}, 1500);
|
||||
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("mismatched device key", async function () {
|
||||
const { aliceRz } = await completeLogin({
|
||||
[userId]: {
|
||||
BOB: "XXXX",
|
||||
},
|
||||
});
|
||||
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
|
||||
});
|
||||
});
|
||||
@@ -1,453 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import type { MatrixClient } from "../../../src";
|
||||
import { LegacyRendezvousFailureReason as RendezvousFailureReason } from "../../../src/rendezvous";
|
||||
import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports";
|
||||
|
||||
function makeMockClient(opts: { userId: string; deviceId: string; msc3886Enabled: boolean }): MatrixClient {
|
||||
return {
|
||||
doesServerSupportUnstableFeature(feature: string) {
|
||||
return Promise.resolve(opts.msc3886Enabled && feature === "org.matrix.msc3886");
|
||||
},
|
||||
getUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getDeviceId() {
|
||||
return opts.deviceId;
|
||||
},
|
||||
requestLoginToken() {
|
||||
return Promise.resolve({ login_token: "token" });
|
||||
},
|
||||
baseUrl: "https://example.com",
|
||||
} as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
describe("SimpleHttpRendezvousTransport", function () {
|
||||
let httpBackend: MockHttpBackend;
|
||||
let fetchFn: typeof global.fetch;
|
||||
|
||||
beforeEach(function () {
|
||||
httpBackend = new MockHttpBackend();
|
||||
fetchFn = httpBackend.fetchFn as typeof global.fetch;
|
||||
});
|
||||
|
||||
async function postAndCheckLocation(
|
||||
msc3886Enabled: boolean,
|
||||
fallbackRzServer: string,
|
||||
locationResponse: string,
|
||||
expectedFinalLocation: string,
|
||||
) {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fallbackRzServer, fetchFn });
|
||||
{
|
||||
// initial POST
|
||||
const expectedPostLocation = msc3886Enabled
|
||||
? `${client.baseUrl}/_matrix/client/unstable/org.matrix.msc3886/rendezvous`
|
||||
: fallbackRzServer;
|
||||
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", expectedPostLocation).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: locationResponse,
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
await prom;
|
||||
}
|
||||
const details = await simpleHttpTransport.details();
|
||||
expect(details.uri).toBe(expectedFinalLocation);
|
||||
|
||||
{
|
||||
// first GET without etag
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", expectedFinalLocation).response = {
|
||||
body: {},
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toEqual({});
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
}
|
||||
}
|
||||
it("should throw an error when no server available", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
|
||||
await expect(simpleHttpTransport.send({})).rejects.toThrow("Invalid rendezvous URI");
|
||||
});
|
||||
|
||||
it("POST to fallback server", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("POST with no location", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await Promise.all([expect(prom).rejects.toThrow(), httpBackend.flush("")]);
|
||||
});
|
||||
|
||||
it("POST with absolute path response", async function () {
|
||||
await postAndCheckLocation(false, "https://fallbackserver/rz", "/123", "https://fallbackserver/123");
|
||||
});
|
||||
|
||||
it("POST to built-in MSC3886 implementation", async function () {
|
||||
await postAndCheckLocation(
|
||||
true,
|
||||
"https://fallbackserver/rz",
|
||||
"123",
|
||||
"https://example.com/_matrix/client/unstable/org.matrix.msc3886/rendezvous/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST with relative path response including parent", async function () {
|
||||
await postAndCheckLocation(
|
||||
false,
|
||||
"https://fallbackserver/rz/abc",
|
||||
"../xyz/123",
|
||||
"https://fallbackserver/rz/xyz/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST to follow 307 to other server", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 307,
|
||||
headers: {
|
||||
location: "https://redirected.fallbackserver/rz",
|
||||
},
|
||||
},
|
||||
};
|
||||
httpBackend.when("POST", "https://redirected.fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://redirected.fallbackserver/rz/123",
|
||||
etag: "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("POST and GET", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{
|
||||
// initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
}
|
||||
{
|
||||
// first GET without etag
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"etag": "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toEqual({ foo: "baa" });
|
||||
}
|
||||
{
|
||||
// subsequent GET which should have etag from previous request
|
||||
const prom = simpleHttpTransport.receive();
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers }) => {
|
||||
expect(headers["if-none-match"]).toEqual("aaa");
|
||||
}).response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"etag": "bbb",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toEqual({ foo: "baa" });
|
||||
}
|
||||
});
|
||||
|
||||
it("POST and PUTs", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{
|
||||
// initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("", 1);
|
||||
await prom;
|
||||
}
|
||||
{
|
||||
// first PUT without etag
|
||||
const prom = simpleHttpTransport.send({ a: "b" });
|
||||
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers, data }) => {
|
||||
expect(headers["if-match"]).toBeUndefined();
|
||||
expect(data).toEqual({ a: "b" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 202,
|
||||
headers: {
|
||||
etag: "aaa",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("", 1);
|
||||
await prom;
|
||||
}
|
||||
{
|
||||
// subsequent PUT which should have etag from previous request
|
||||
const prom = simpleHttpTransport.send({ c: "d" });
|
||||
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => {
|
||||
expect(headers["if-match"]).toEqual("aaa");
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 202,
|
||||
headers: {
|
||||
etag: "bbb",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("", 1);
|
||||
await prom;
|
||||
}
|
||||
});
|
||||
|
||||
it("POST and DELETE", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
{
|
||||
// Create
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
|
||||
expect(headers["content-type"]).toEqual("application/json");
|
||||
expect(data).toEqual({ foo: "baa" });
|
||||
}).response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(await prom).toStrictEqual(undefined);
|
||||
}
|
||||
{
|
||||
// Cancel
|
||||
const prom = simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
|
||||
httpBackend.when("DELETE", "https://fallbackserver/rz/123").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
await prom;
|
||||
}
|
||||
});
|
||||
|
||||
it("details before ready", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
await expect(simpleHttpTransport.details()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("send after cancelled", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
await simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
|
||||
await expect(simpleHttpTransport.send({})).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("receive before ready", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
await expect(simpleHttpTransport.receive()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("404 failure callback", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const onFailure = jest.fn();
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
onFailure,
|
||||
});
|
||||
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 404,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await Promise.all([
|
||||
expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined(),
|
||||
httpBackend.flush("", 1),
|
||||
]);
|
||||
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("404 failure callback mapped to expired", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const onFailure = jest.fn();
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
client,
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
onFailure,
|
||||
});
|
||||
|
||||
{
|
||||
// initial POST
|
||||
const prom = simpleHttpTransport.send({ foo: "baa" });
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
statusCode: 201,
|
||||
headers: {
|
||||
location: "https://fallbackserver/rz/123",
|
||||
expires: "Thu, 01 Jan 1970 00:00:00 GMT",
|
||||
},
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
await prom;
|
||||
}
|
||||
{
|
||||
// GET with 404 to simulate expiry
|
||||
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
|
||||
body: { foo: "baa" },
|
||||
response: {
|
||||
statusCode: 404,
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
await Promise.all([expect(simpleHttpTransport.receive()).resolves.toBeUndefined(), httpBackend.flush("")]);
|
||||
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Expired);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -208,7 +208,7 @@ describe("OutgoingRequestProcessor", () => {
|
||||
const outgoingRequest = new UploadSigningKeysRequest(JSON.stringify(testReq));
|
||||
|
||||
// also create a UIA callback
|
||||
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
|
||||
const authCallback: UIAuthCallback<object> = async (makeRequest) => {
|
||||
return await makeRequest({ type: "test" });
|
||||
};
|
||||
|
||||
@@ -458,7 +458,7 @@ describe("OutgoingRequestProcessor", () => {
|
||||
throw new Error("Failed to fetch");
|
||||
}
|
||||
});
|
||||
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
|
||||
const authCallback: UIAuthCallback<object> = async (makeRequest) => {
|
||||
return await makeRequest({ type: "test" });
|
||||
};
|
||||
const requestPromise = processor.makeOutgoingRequest(outgoingRequest, authCallback);
|
||||
|
||||
@@ -26,7 +26,6 @@ import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } f
|
||||
import * as TestData from "../../test-utils/test-data";
|
||||
import {
|
||||
ConnectionError,
|
||||
CryptoEvent,
|
||||
HttpApiEvent,
|
||||
HttpApiEventHandlerMap,
|
||||
IHttpOpts,
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { BackupDecryptor } from "../../../src/common-crypto/CryptoBackend";
|
||||
import { KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
|
||||
|
||||
describe("PerSessionKeyBackupDownloader", () => {
|
||||
/** The downloader under test */
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
CollectStrategy,
|
||||
Curve25519PublicKey,
|
||||
Ed25519PublicKey,
|
||||
HistoryVisibility as RustHistoryVisibility,
|
||||
@@ -31,6 +32,7 @@ import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
|
||||
import { defer } from "../../../src/utils";
|
||||
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
import { DeviceIsolationMode, AllDevicesIsolationMode, OnlySignedDevicesIsolationMode } from "../../../src/crypto-api";
|
||||
|
||||
describe("RoomEncryptor", () => {
|
||||
describe("History Visibility", () => {
|
||||
@@ -99,7 +101,7 @@ describe("RoomEncryptor", () => {
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue([mockRoomMember]),
|
||||
shouldEncryptForInvitedMembers: jest.fn().mockReturnValue(true),
|
||||
getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Invited),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(null),
|
||||
} as unknown as Mocked<Room>;
|
||||
|
||||
roomEncryptor = new RoomEncryptor(
|
||||
@@ -111,6 +113,8 @@ describe("RoomEncryptor", () => {
|
||||
);
|
||||
});
|
||||
|
||||
const defaultDevicesIsolationMode = new AllDevicesIsolationMode(false);
|
||||
|
||||
it("should ensure that there is only one shareRoomKey at a time", async () => {
|
||||
const deferredShare = defer<void>();
|
||||
const insideOlmShareRoom = defer<void>();
|
||||
@@ -120,19 +124,19 @@ describe("RoomEncryptor", () => {
|
||||
await deferredShare.promise;
|
||||
});
|
||||
|
||||
roomEncryptor.prepareForEncryption(false);
|
||||
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
|
||||
await insideOlmShareRoom.promise;
|
||||
|
||||
// call several times more
|
||||
roomEncryptor.prepareForEncryption(false);
|
||||
roomEncryptor.encryptEvent(createMockEvent("Hello"), false);
|
||||
roomEncryptor.prepareForEncryption(false);
|
||||
roomEncryptor.encryptEvent(createMockEvent("World"), false);
|
||||
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
|
||||
roomEncryptor.encryptEvent(createMockEvent("Hello"), false, defaultDevicesIsolationMode);
|
||||
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
|
||||
roomEncryptor.encryptEvent(createMockEvent("World"), false, defaultDevicesIsolationMode);
|
||||
|
||||
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalledTimes(1);
|
||||
|
||||
deferredShare.resolve();
|
||||
await roomEncryptor.prepareForEncryption(false);
|
||||
await roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
|
||||
|
||||
// should have been called again
|
||||
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalledTimes(6);
|
||||
@@ -158,8 +162,16 @@ describe("RoomEncryptor", () => {
|
||||
|
||||
let firstMessageFinished: string | null = null;
|
||||
|
||||
const firstRequest = roomEncryptor.encryptEvent(createMockEvent("Hello"), false);
|
||||
const secondRequest = roomEncryptor.encryptEvent(createMockEvent("Edit of Hello"), false);
|
||||
const firstRequest = roomEncryptor.encryptEvent(
|
||||
createMockEvent("Hello"),
|
||||
false,
|
||||
defaultDevicesIsolationMode,
|
||||
);
|
||||
const secondRequest = roomEncryptor.encryptEvent(
|
||||
createMockEvent("Edit of Hello"),
|
||||
false,
|
||||
defaultDevicesIsolationMode,
|
||||
);
|
||||
|
||||
firstRequest.then(() => {
|
||||
if (firstMessageFinished === null) {
|
||||
@@ -181,5 +193,96 @@ describe("RoomEncryptor", () => {
|
||||
|
||||
expect(firstMessageFinished).toBe("hello");
|
||||
});
|
||||
|
||||
describe("DeviceIsolationMode", () => {
|
||||
type TestCase = [
|
||||
string,
|
||||
{
|
||||
mode: DeviceIsolationMode;
|
||||
expectedStrategy: CollectStrategy;
|
||||
globalBlacklistUnverifiedDevices: boolean;
|
||||
},
|
||||
];
|
||||
|
||||
const testCases: TestCase[] = [
|
||||
[
|
||||
"Share AllDevicesIsolationMode",
|
||||
{
|
||||
mode: new AllDevicesIsolationMode(false),
|
||||
expectedStrategy: CollectStrategy.deviceBasedStrategy(false, false),
|
||||
globalBlacklistUnverifiedDevices: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Share AllDevicesIsolationMode - with blacklist unverified",
|
||||
{
|
||||
mode: new AllDevicesIsolationMode(false),
|
||||
expectedStrategy: CollectStrategy.deviceBasedStrategy(true, false),
|
||||
globalBlacklistUnverifiedDevices: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Share OnlySigned - blacklist true",
|
||||
{
|
||||
mode: new OnlySignedDevicesIsolationMode(),
|
||||
expectedStrategy: CollectStrategy.identityBasedStrategy(),
|
||||
globalBlacklistUnverifiedDevices: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Share OnlySigned",
|
||||
{
|
||||
mode: new OnlySignedDevicesIsolationMode(),
|
||||
expectedStrategy: CollectStrategy.identityBasedStrategy(),
|
||||
globalBlacklistUnverifiedDevices: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Share AllDevicesIsolationMode - Verified user problems true",
|
||||
{
|
||||
mode: new AllDevicesIsolationMode(true),
|
||||
expectedStrategy: CollectStrategy.deviceBasedStrategy(false, true),
|
||||
globalBlacklistUnverifiedDevices: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'Share AllDevicesIsolationMode - with blacklist unverified - Verified user problems true"',
|
||||
{
|
||||
mode: new AllDevicesIsolationMode(true),
|
||||
expectedStrategy: CollectStrategy.deviceBasedStrategy(true, true),
|
||||
globalBlacklistUnverifiedDevices: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
let capturedSettings: CollectStrategy | undefined = undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
capturedSettings = undefined;
|
||||
mockOlmMachine.shareRoomKey.mockImplementationOnce(async (roomId, users, encryptionSettings) => {
|
||||
capturedSettings = encryptionSettings.sharingStrategy;
|
||||
});
|
||||
});
|
||||
|
||||
it.each(testCases)(
|
||||
"prepareForEncryption should properly set sharing strategy based on crypto mode: %s",
|
||||
async (_, { mode, expectedStrategy, globalBlacklistUnverifiedDevices }) => {
|
||||
await roomEncryptor.prepareForEncryption(globalBlacklistUnverifiedDevices, mode);
|
||||
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalled();
|
||||
expect(capturedSettings).toBeDefined();
|
||||
expect(expectedStrategy.eq(capturedSettings!)).toBeTruthy();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(testCases)(
|
||||
"encryptEvent should properly set sharing strategy based on crypto mode: %s",
|
||||
async (_, { mode, expectedStrategy, globalBlacklistUnverifiedDevices }) => {
|
||||
await roomEncryptor.encryptEvent(createMockEvent("Hello"), globalBlacklistUnverifiedDevices, mode);
|
||||
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalled();
|
||||
expect(capturedSettings).toBeDefined();
|
||||
expect(expectedStrategy.eq(capturedSettings!)).toBeTruthy();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Mocked } from "jest-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { CryptoEvent, HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, TypedEventEmitter } from "../../../src";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, TypedEventEmitter } from "../../../src";
|
||||
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import * as TestData from "../../test-utils/test-data";
|
||||
|
||||
@@ -30,7 +30,6 @@ import fetchMock from "fetch-mock-jest";
|
||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../../src/rust-crypto";
|
||||
import {
|
||||
CryptoEvent,
|
||||
Device,
|
||||
DeviceVerification,
|
||||
encodeBase64,
|
||||
@@ -69,8 +68,9 @@ import { logger } from "../../../src/logger";
|
||||
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
|
||||
import { ClientEvent, ClientEventHandlerMap } from "../../../src/client";
|
||||
import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
|
||||
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
|
||||
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
|
||||
|
||||
const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
@@ -95,6 +95,7 @@ describe("initRustCrypto", () => {
|
||||
deleteSecretsFromInbox: jest.fn(),
|
||||
registerReceiveSecretCallback: jest.fn(),
|
||||
registerDevicesUpdatedCallback: jest.fn(),
|
||||
registerRoomKeysWithheldCallback: jest.fn(),
|
||||
outgoingRequests: jest.fn(),
|
||||
isBackupEnabled: jest.fn().mockResolvedValue(false),
|
||||
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
|
||||
@@ -424,7 +425,7 @@ describe("initRustCrypto", () => {
|
||||
}, 10000);
|
||||
|
||||
async function encryptAndStoreSecretKey(type: string, key: Uint8Array, pickleKey: string, store: CryptoStore) {
|
||||
const encryptedKey = await encryptAES(encodeBase64(key), Buffer.from(pickleKey), type);
|
||||
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), Buffer.from(pickleKey), type);
|
||||
store.storeSecretStorePrivateKey(undefined, type as keyof SecretStorePrivateKeys, encryptedKey);
|
||||
}
|
||||
|
||||
@@ -1361,13 +1362,52 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
|
||||
it("returns a verified UserVerificationStatus when the UserIdentity is verified", async () => {
|
||||
olmMachine.getIdentity.mockResolvedValue({ free: jest.fn(), isVerified: jest.fn().mockReturnValue(true) });
|
||||
olmMachine.getIdentity.mockResolvedValue({
|
||||
free: jest.fn(),
|
||||
isVerified: jest.fn().mockReturnValue(true),
|
||||
wasPreviouslyVerified: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
|
||||
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
|
||||
expect(userVerificationStatus.isVerified()).toBeTruthy();
|
||||
expect(userVerificationStatus.isTofu()).toBeFalsy();
|
||||
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(userVerificationStatus.wasCrossSigningVerified()).toBeFalsy();
|
||||
expect(userVerificationStatus.wasCrossSigningVerified()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("pinCurrentIdentity", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
beforeEach(() => {
|
||||
olmMachine = {
|
||||
getIdentity: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
rustCrypto = new RustCrypto(
|
||||
logger,
|
||||
olmMachine,
|
||||
{} as MatrixClient["http"],
|
||||
TEST_USER,
|
||||
TEST_DEVICE_ID,
|
||||
{} as ServerSideSecretStorage,
|
||||
{} as CryptoCallbacks,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws an error for an unknown user", async () => {
|
||||
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
|
||||
"Cannot pin identity of unknown user",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws an error for our own user", async () => {
|
||||
const ownIdentity = new RustSdkCryptoJs.OwnUserIdentity();
|
||||
olmMachine.getIdentity.mockResolvedValue(ownIdentity);
|
||||
|
||||
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
|
||||
"Cannot pin identity of own user",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1553,6 +1593,124 @@ describe("RustCrypto", () => {
|
||||
await expect(rustCrypto.exportSecretsBundle()).resolves.toEqual(expect.objectContaining(bundle));
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptToDeviceMessages", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
let testOlmMachine: RustSdkCryptoJs.OlmMachine;
|
||||
|
||||
beforeEach(async () => {
|
||||
testOlmMachine = await OlmMachine.initialize(
|
||||
new RustSdkCryptoJs.UserId(testData.TEST_USER_ID),
|
||||
new RustSdkCryptoJs.DeviceId(testData.TEST_DEVICE_ID),
|
||||
);
|
||||
jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine);
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
expect(OlmMachine.initFromStore).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testOlmMachine?.free();
|
||||
});
|
||||
|
||||
const payload = { hello: "world" };
|
||||
|
||||
it("returns empty batch if devices not known", async () => {
|
||||
const getMissingSessions = jest.spyOn(testOlmMachine, "getMissingSessions");
|
||||
const getDevice = jest.spyOn(testOlmMachine, "getDevice");
|
||||
const batch = await rustCrypto.encryptToDeviceMessages(
|
||||
"m.test.type",
|
||||
[
|
||||
{ deviceId: "AAA", userId: "@user1:domain" },
|
||||
{ deviceId: "BBB", userId: "@user1:domain" },
|
||||
{ deviceId: "CCC", userId: "@user2:domain" },
|
||||
],
|
||||
payload,
|
||||
);
|
||||
expect(getMissingSessions.mock.calls[0][0].length).toBe(2);
|
||||
expect(getDevice).toHaveBeenCalledTimes(3);
|
||||
expect(batch?.eventType).toEqual("m.room.encrypted");
|
||||
expect(batch?.batch).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns encrypted batch for known devices", async () => {
|
||||
// Make m aware of another device, and get some OTK to be able to establish a session.
|
||||
await testOlmMachine.markRequestAsSent(
|
||||
"foo",
|
||||
RustSdkCryptoJs.RequestType.KeysQuery,
|
||||
JSON.stringify({
|
||||
device_keys: {
|
||||
"@example:localhost": {
|
||||
AFGUOBTZWM: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "AFGUOBTZWM",
|
||||
keys: {
|
||||
"curve25519:AFGUOBTZWM": "boYjDpaC+7NkECQEeMh5dC+I1+AfriX0VXG2UV7EUQo",
|
||||
"ed25519:AFGUOBTZWM": "NayrMQ33ObqMRqz6R9GosmHdT6HQ6b/RX/3QlZ2yiec",
|
||||
},
|
||||
signatures: {
|
||||
"@example:localhost": {
|
||||
"ed25519:AFGUOBTZWM":
|
||||
"RoSWvru1jj6fs2arnTedWsyIyBmKHMdOu7r9gDi0BZ61h9SbCK2zLXzuJ9ZFLao2VvA0yEd7CASCmDHDLYpXCA",
|
||||
},
|
||||
},
|
||||
user_id: "@example:localhost",
|
||||
unsigned: {
|
||||
device_display_name: "rust-sdk",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
}),
|
||||
);
|
||||
|
||||
await testOlmMachine.markRequestAsSent(
|
||||
"bar",
|
||||
RustSdkCryptoJs.RequestType.KeysClaim,
|
||||
JSON.stringify({
|
||||
one_time_keys: {
|
||||
"@example:localhost": {
|
||||
AFGUOBTZWM: {
|
||||
"signed_curve25519:AAAABQ": {
|
||||
key: "9IGouMnkB6c6HOd4xUsNv4i3Dulb4IS96TzDordzOws",
|
||||
signatures: {
|
||||
"@example:localhost": {
|
||||
"ed25519:AFGUOBTZWM":
|
||||
"2bvUbbmJegrV0eVP/vcJKuIWC3kud+V8+C0dZtg4dVovOSJdTP/iF36tQn2bh5+rb9xLlSeztXBdhy4c+LiOAg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
}),
|
||||
);
|
||||
|
||||
const batch = await rustCrypto.encryptToDeviceMessages(
|
||||
"m.test.type",
|
||||
[
|
||||
{ deviceId: "AAA", userId: "@user1:domain" },
|
||||
{ deviceId: "BBB", userId: "@user1:domain" },
|
||||
{ deviceId: "CCC", userId: "@user2:domain" },
|
||||
{ deviceId: "AFGUOBTZWM", userId: "@example:localhost" },
|
||||
],
|
||||
payload,
|
||||
);
|
||||
expect(batch?.eventType).toEqual("m.room.encrypted");
|
||||
expect(batch?.batch.length).toEqual(1);
|
||||
expect(batch?.batch[0].deviceId).toEqual("AFGUOBTZWM");
|
||||
expect(batch?.batch[0].userId).toEqual("@example:localhost");
|
||||
expect(batch?.batch[0].payload).toEqual(
|
||||
expect.objectContaining({
|
||||
"algorithm": "m.olm.v1.curve25519-aes-sha2",
|
||||
"ciphertext": expect.any(Object),
|
||||
"org.matrix.msgid": expect.any(String),
|
||||
"sender_key": expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** Build a MatrixHttpApi instance */
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("secret-storage", () => {
|
||||
} else if (secretName == "secretE") {
|
||||
return { aaaa: {}, bbbb: {} };
|
||||
} else {
|
||||
null;
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue("aaaa"),
|
||||
|
||||
@@ -12,7 +12,7 @@ jest.useFakeTimers();
|
||||
|
||||
describe("MatrixScheduler", function () {
|
||||
let scheduler: MatrixScheduler<Record<string, boolean>>;
|
||||
let retryFn: Function | null;
|
||||
let retryFn: ((event: MatrixEvent | null, attempt: number, err: MatrixError) => number) | null;
|
||||
let queueFn: ((event: MatrixEvent) => string | null) | null;
|
||||
let deferred: IDeferred<Record<string, boolean>>;
|
||||
const roomId = "!foo:bar";
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
AccountDataClient,
|
||||
calculateKeyCheck,
|
||||
PassphraseInfo,
|
||||
SecretStorageCallbacks,
|
||||
SecretStorageKeyDescriptionAesV1,
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
ServerSideSecretStorageImpl,
|
||||
trimTrailingEquals,
|
||||
} from "../../src/secret-storage";
|
||||
import { calculateKeyCheck } from "../../src/crypto/aes";
|
||||
import { randomString } from "../../src/randomstring";
|
||||
|
||||
describe("ServerSideSecretStorageImpl", function () {
|
||||
|
||||
@@ -434,7 +434,7 @@ describe("TimelineWindow", function () {
|
||||
});
|
||||
|
||||
function idsOf(events: Array<MatrixEvent>): Array<string> {
|
||||
return events.map((e) => (e ? e.getId() ?? "MISSING_ID" : "MISSING_EVENT"));
|
||||
return events.map((e) => (e ? (e.getId() ?? "MISSING_ID") : "MISSING_EVENT"));
|
||||
}
|
||||
|
||||
describe("removing events", () => {
|
||||
|
||||
@@ -1228,7 +1228,7 @@ describe("Call", function () {
|
||||
});
|
||||
|
||||
describe("Screen sharing", () => {
|
||||
const waitNegotiateFunc = (resolve: Function): void => {
|
||||
const waitNegotiateFunc = (resolve: () => void): void => {
|
||||
mockSendEvent.mockImplementationOnce(() => {
|
||||
// Note that the peer connection here is a dummy one and always returns
|
||||
// dummy SDP, so there's not much point returning the content: the SDP will
|
||||
|
||||
@@ -101,7 +101,7 @@ describe("CallFeed", () => {
|
||||
it.each([
|
||||
[CallState.Connected, true],
|
||||
[CallState.Connecting, false],
|
||||
])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => {
|
||||
])("should react to call state, when !isLocal()", (state: CallState, expected: boolean) => {
|
||||
call.emit(CallEvent.State, state, CallState.InviteSent, call.typed());
|
||||
|
||||
expect(feed.connected).toBe(expected);
|
||||
|
||||
@@ -105,7 +105,7 @@ const mockGetStateEvents =
|
||||
(events: MatrixEvent[] = FAKE_STATE_EVENTS as MatrixEvent[]) =>
|
||||
(type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
|
||||
if (type === EventType.GroupCallMemberPrefix) {
|
||||
return userId === undefined ? events : events.find((e) => e.getStateKey() === userId) ?? null;
|
||||
return userId === undefined ? events : (events.find((e) => e.getStateKey() === userId) ?? null);
|
||||
} else {
|
||||
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
|
||||
return userId === undefined ? [fakeEvent] : fakeEvent;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An AES-encrypted secret storage payload.
|
||||
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2-1
|
||||
*/
|
||||
export interface AESEncryptedSecretStoragePayload {
|
||||
[key: string]: any; // extensible
|
||||
/** the initialization vector in base64 */
|
||||
iv: string;
|
||||
/** the ciphertext in base64 */
|
||||
ciphertext: string;
|
||||
/** the HMAC in base64 */
|
||||
mac: string;
|
||||
}
|
||||
+2
-2
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IClientWellKnown } from "../client";
|
||||
import { UnstableValue } from "../NamespacedValue.ts";
|
||||
import { IClientWellKnown } from "../client.ts";
|
||||
|
||||
// disable lint because these are wire responses
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { RelatesToRelationship, REFERENCE_RELATION } from "./extensible_events";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
|
||||
import { RelatesToRelationship, REFERENCE_RELATION } from "./extensible_events.ts";
|
||||
import { UnstableValue } from "../NamespacedValue.ts";
|
||||
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location.ts";
|
||||
|
||||
/**
|
||||
* Beacon info and beacon event types as described in MSC3672
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ISignatures } from "./signed";
|
||||
import type { ISignatures } from "./signed.ts";
|
||||
|
||||
export type OlmGroupSessionExtraData = {
|
||||
untrusted?: boolean;
|
||||
@@ -22,7 +22,7 @@ export type OlmGroupSessionExtraData = {
|
||||
};
|
||||
|
||||
// Backwards compatible re-export
|
||||
export type { EventDecryptionResult as IEventDecryptionResult } from "../common-crypto/CryptoBackend";
|
||||
export type { EventDecryptionResult as IEventDecryptionResult } from "../common-crypto/CryptoBackend.ts";
|
||||
|
||||
interface Extensible {
|
||||
[key: string]: any;
|
||||
|
||||
+11
-11
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { NamespacedValue, UnstableValue } from "../NamespacedValue";
|
||||
import { NamespacedValue, UnstableValue } from "../NamespacedValue.ts";
|
||||
import {
|
||||
PolicyRuleEventContent,
|
||||
RoomAvatarEventContent,
|
||||
@@ -34,16 +34,16 @@ import {
|
||||
RoomTopicEventContent,
|
||||
SpaceChildEventContent,
|
||||
SpaceParentEventContent,
|
||||
} from "./state_events";
|
||||
} from "./state_events.ts";
|
||||
import {
|
||||
ExperimentalGroupCallRoomMemberState,
|
||||
IGroupCallRoomMemberState,
|
||||
IGroupCallRoomState,
|
||||
} from "../webrtc/groupCall";
|
||||
import { MSC3089EventContent } from "../models/MSC3089Branch";
|
||||
import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon";
|
||||
import { XOR } from "./common";
|
||||
import { ReactionEventContent, RoomMessageEventContent, StickerEventContent } from "./events";
|
||||
} from "../webrtc/groupCall.ts";
|
||||
import { MSC3089EventContent } from "../models/MSC3089Branch.ts";
|
||||
import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon.ts";
|
||||
import { XOR } from "./common.ts";
|
||||
import { ReactionEventContent, RoomMessageEventContent, StickerEventContent } from "./events.ts";
|
||||
import {
|
||||
MCallAnswer,
|
||||
MCallBase,
|
||||
@@ -54,10 +54,10 @@ import {
|
||||
MCallSelectAnswer,
|
||||
SDPStreamMetadata,
|
||||
SDPStreamMetadataKey,
|
||||
} from "../webrtc/callEventTypes";
|
||||
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types";
|
||||
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls";
|
||||
import { SessionMembershipData } from "../matrixrtc/CallMembership";
|
||||
} from "../webrtc/callEventTypes.ts";
|
||||
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types.ts";
|
||||
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls.ts";
|
||||
import { SessionMembershipData } from "../matrixrtc/CallMembership.ts";
|
||||
|
||||
export enum EventType {
|
||||
// Room state events
|
||||
|
||||
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MsgType, RelationType } from "./event";
|
||||
import { FileInfo, ImageInfo, MediaEventContent } from "./media";
|
||||
import { XOR } from "./common";
|
||||
import { MsgType, RelationType } from "./event.ts";
|
||||
import { FileInfo, ImageInfo, MediaEventContent } from "./media.ts";
|
||||
import { XOR } from "./common.ts";
|
||||
|
||||
interface BaseTimelineEvent {
|
||||
"body": string;
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { EitherAnd, NamespacedValue, Optional, UnstableValue } from "matrix-events-sdk";
|
||||
|
||||
import { isProvided } from "../extensible_events_v1/utilities";
|
||||
import { isProvided } from "../extensible_events_v1/utilities.ts";
|
||||
|
||||
// Types and utilities for MSC1767: Extensible events (version 1) in Matrix
|
||||
|
||||
|
||||
Vendored
-45
@@ -29,20 +29,11 @@ declare global {
|
||||
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
localStorage: Storage;
|
||||
// marker variable used to detect both the browser & node entrypoints being used at once
|
||||
__js_sdk_entrypoint: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
webkitAudioContext: typeof AudioContext;
|
||||
}
|
||||
|
||||
interface Crypto {
|
||||
webkitSubtle?: Window["crypto"]["subtle"];
|
||||
}
|
||||
|
||||
interface MediaDevices {
|
||||
// This is experimental and types don't know about it yet
|
||||
// https://github.com/microsoft/TypeScript/issues/33232
|
||||
@@ -76,40 +67,4 @@ declare global {
|
||||
// on webkit: we should check if we still need to do this
|
||||
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
|
||||
}
|
||||
|
||||
export interface ISettledFulfilled<T> {
|
||||
status: "fulfilled";
|
||||
value: T;
|
||||
}
|
||||
export interface ISettledRejected {
|
||||
status: "rejected";
|
||||
reason: any;
|
||||
}
|
||||
|
||||
interface PromiseConstructor {
|
||||
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
|
||||
}
|
||||
|
||||
interface RTCRtpTransceiver {
|
||||
// This has been removed from TS
|
||||
// (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029),
|
||||
// but we still need this for MatrixCall::getRidOfRTXCodecs()
|
||||
setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
|
||||
}
|
||||
|
||||
interface RequestInit {
|
||||
/**
|
||||
* Specifies the priority of the fetch request relative to other requests of the same type.
|
||||
* Must be one of the following strings:
|
||||
* high: A high priority fetch request relative to other requests of the same type.
|
||||
* low: A low priority fetch request relative to other requests of the same type.
|
||||
* auto: Automatically determine the priority of the fetch request relative to other requests of the same type (default).
|
||||
*
|
||||
* @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attribute
|
||||
* @see https://github.com/microsoft/TypeScript/issues/54472
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#browser_compatibility
|
||||
* Not yet supported in Safari or Firefox
|
||||
*/
|
||||
priority?: "high" | "low" | "auto";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ limitations under the License.
|
||||
// Types for MSC3488 - m.location: Extending events with location data
|
||||
import { EitherAnd } from "matrix-events-sdk";
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { M_TEXT } from "./extensible_events";
|
||||
import { UnstableValue } from "../NamespacedValue.ts";
|
||||
import { M_TEXT } from "./extensible_events.ts";
|
||||
|
||||
export enum LocationAssetType {
|
||||
Self = "m.self",
|
||||
|
||||
+30
-37
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MsgType } from "../@types/event";
|
||||
import { MsgType } from "../@types/event.ts";
|
||||
|
||||
/**
|
||||
* Information on encrypted media attachments.
|
||||
*
|
||||
* Used within `m.room.message` events that reference files, such as `m.file` and `m.image`.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#extensions-to-mroommessage-msgtypes
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#extensions-to-mroommessage-msgtypes
|
||||
*/
|
||||
export interface EncryptedFile {
|
||||
/**
|
||||
@@ -84,13 +84,13 @@ interface BaseInfo {
|
||||
*
|
||||
* Used within `m.room.message` events that reference files.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mfile
|
||||
*/
|
||||
export interface FileInfo extends BaseInfo {
|
||||
/**
|
||||
* Information on the encrypted thumbnail file, as specified in End-to-end encryption.
|
||||
* Only present if the thumbnail is encrypted.
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#sending-encrypted-attachments
|
||||
*/
|
||||
thumbnail_file?: EncryptedFile;
|
||||
/**
|
||||
@@ -108,7 +108,7 @@ export interface FileInfo extends BaseInfo {
|
||||
*
|
||||
* Used within `m.room.message` events that reference images.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mimage
|
||||
*/
|
||||
export interface ImageInfo extends FileInfo, ThumbnailInfo {}
|
||||
|
||||
@@ -117,7 +117,7 @@ export interface ImageInfo extends FileInfo, ThumbnailInfo {}
|
||||
*
|
||||
* Used within `m.room.message` events that reference audio files.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#maudio
|
||||
*/
|
||||
export interface AudioInfo extends BaseInfo {
|
||||
/**
|
||||
@@ -131,7 +131,7 @@ export interface AudioInfo extends BaseInfo {
|
||||
*
|
||||
* Used within `m.room.message` events that reference video files.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mvideo
|
||||
*/
|
||||
export interface VideoInfo extends AudioInfo, ImageInfo {
|
||||
/**
|
||||
@@ -148,30 +148,39 @@ export type MediaEventInfo = FileInfo | ImageInfo | AudioInfo | VideoInfo;
|
||||
interface BaseContent {
|
||||
/**
|
||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#sending-encrypted-attachments
|
||||
*/
|
||||
file?: EncryptedFile;
|
||||
/**
|
||||
* Required if the file is unencrypted. The URL (typically mxc:// URI) to the file.
|
||||
*/
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of media events with msgtype `m.file`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
|
||||
*/
|
||||
export interface FileContent extends BaseContent {
|
||||
/**
|
||||
* A human-readable description of the file.
|
||||
* This is recommended to be the filename of the original upload.
|
||||
* If filename is not set or the value of both properties are identical,
|
||||
* this is the filename of the original upload. Otherwise, this is a
|
||||
* caption for the file.
|
||||
*/
|
||||
body: string;
|
||||
/**
|
||||
* The original filename of the uploaded file.
|
||||
*/
|
||||
filename?: string;
|
||||
/**
|
||||
* The format used in the `formatted_body`.
|
||||
*/
|
||||
format?: "org.matrix.custom.html";
|
||||
/**
|
||||
* The formatted version of the `body`, when it acts as a caption. This is required if `format` is specified.
|
||||
*/
|
||||
formatted_body?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of media events with msgtype `m.file`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mfile
|
||||
*/
|
||||
export interface FileContent extends BaseContent {
|
||||
/**
|
||||
* Information about the file referred to in url.
|
||||
*/
|
||||
@@ -185,15 +194,9 @@ export interface FileContent extends BaseContent {
|
||||
/**
|
||||
* Content format of media events with msgtype `m.image`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mimage
|
||||
*/
|
||||
export interface ImageContent extends BaseContent {
|
||||
/**
|
||||
* A textual representation of the image.
|
||||
* This could be the alt text of the image, the filename of the image,
|
||||
* or some kind of content description for accessibility e.g. ‘image attachment’.
|
||||
*/
|
||||
body: string;
|
||||
/**
|
||||
* Metadata about the image referred to in url.
|
||||
*/
|
||||
@@ -207,14 +210,9 @@ export interface ImageContent extends BaseContent {
|
||||
/**
|
||||
* Content format of media events with msgtype `m.audio`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#maudio
|
||||
*/
|
||||
export interface AudioContent extends BaseContent {
|
||||
/**
|
||||
* A description of the audio e.g. ‘Bee Gees - Stayin’ Alive’,
|
||||
* or some kind of content description for accessibility e.g. ‘audio attachment’.
|
||||
*/
|
||||
body: string;
|
||||
/**
|
||||
* Metadata for the audio clip referred to in url.
|
||||
*/
|
||||
@@ -228,14 +226,9 @@ export interface AudioContent extends BaseContent {
|
||||
/**
|
||||
* Content format of media events with msgtype `m.video`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mvideo
|
||||
*/
|
||||
export interface VideoContent extends BaseContent {
|
||||
/**
|
||||
* A description of the video e.g. ‘Gangnam style’,
|
||||
* or some kind of content description for accessibility e.g. ‘video attachment’.
|
||||
*/
|
||||
body: string;
|
||||
/**
|
||||
* Metadata about the video clip referred to in url.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import {
|
||||
REFERENCE_RELATION,
|
||||
RelatesToRelationship,
|
||||
TSNamespace,
|
||||
} from "./extensible_events";
|
||||
} from "./extensible_events.ts";
|
||||
|
||||
/**
|
||||
* Identifier for a disclosed poll.
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { AuthDict } from "../interactive-auth";
|
||||
import { AuthDict } from "../interactive-auth.ts";
|
||||
|
||||
/**
|
||||
* The request body of a call to `POST /_matrix/client/v3/register`.
|
||||
|
||||
+56
-8
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IContent, IEvent } from "../models/event";
|
||||
import { Preset, Visibility } from "./partials";
|
||||
import { IEventWithRoomId, SearchKey } from "./search";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
import { Direction } from "../models/event-timeline";
|
||||
import { PushRuleAction } from "./PushRules";
|
||||
import { IRoomEvent } from "../sync-accumulator";
|
||||
import { EventType, RelationType, RoomType } from "./event";
|
||||
import { IContent, IEvent } from "../models/event.ts";
|
||||
import { Preset, Visibility } from "./partials.ts";
|
||||
import { IEventWithRoomId, SearchKey } from "./search.ts";
|
||||
import { IRoomEventFilter } from "../filter.ts";
|
||||
import { Direction } from "../models/event-timeline.ts";
|
||||
import { PushRuleAction } from "./PushRules.ts";
|
||||
import { IRoomEvent } from "../sync-accumulator.ts";
|
||||
import { EventType, RelationType, RoomType } from "./event.ts";
|
||||
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
@@ -76,6 +76,54 @@ export interface ISendEventResponse {
|
||||
event_id: string;
|
||||
}
|
||||
|
||||
export type TimeoutDelay = {
|
||||
delay: number;
|
||||
};
|
||||
|
||||
export type ParentDelayId = {
|
||||
parent_delay_id: string;
|
||||
};
|
||||
|
||||
export type SendTimeoutDelayedEventRequestOpts = TimeoutDelay & Partial<ParentDelayId>;
|
||||
export type SendActionDelayedEventRequestOpts = ParentDelayId;
|
||||
|
||||
export type SendDelayedEventRequestOpts = SendTimeoutDelayedEventRequestOpts | SendActionDelayedEventRequestOpts;
|
||||
|
||||
export type SendDelayedEventResponse = {
|
||||
delay_id: string;
|
||||
};
|
||||
|
||||
export enum UpdateDelayedEventAction {
|
||||
Cancel = "cancel",
|
||||
Restart = "restart",
|
||||
Send = "send",
|
||||
}
|
||||
|
||||
export type UpdateDelayedEventRequestOpts = SendDelayedEventResponse & {
|
||||
action: UpdateDelayedEventAction;
|
||||
};
|
||||
|
||||
type DelayedPartialTimelineEvent = {
|
||||
room_id: string;
|
||||
type: string;
|
||||
content: IContent;
|
||||
};
|
||||
|
||||
type DelayedPartialStateEvent = DelayedPartialTimelineEvent & {
|
||||
state_key: string;
|
||||
};
|
||||
|
||||
type DelayedPartialEvent = DelayedPartialTimelineEvent | DelayedPartialStateEvent;
|
||||
|
||||
export type DelayedEventInfo = {
|
||||
delayed_events: (DelayedPartialEvent &
|
||||
SendDelayedEventResponse &
|
||||
SendDelayedEventRequestOpts & {
|
||||
running_since: number;
|
||||
})[];
|
||||
next_batch?: string;
|
||||
};
|
||||
|
||||
export interface IPresenceOpts {
|
||||
// One of "online", "offline" or "unavailable"
|
||||
presence: "online" | "offline" | "unavailable";
|
||||
|
||||
@@ -16,9 +16,9 @@ limitations under the License.
|
||||
|
||||
// Types relating to the /search API
|
||||
|
||||
import { IRoomEvent, IStateEvent } from "../sync-accumulator";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
import { SearchResult } from "../models/search-result";
|
||||
import { IRoomEvent, IStateEvent } from "../sync-accumulator.ts";
|
||||
import { IRoomEventFilter } from "../filter.ts";
|
||||
import { SearchResult } from "../models/search-result.ts";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IEventWithRoomId extends IRoomEvent {
|
||||
|
||||
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IPublicRoomsChunkRoom } from "../client";
|
||||
import { RoomType } from "./event";
|
||||
import { IStrippedState } from "../sync-accumulator";
|
||||
import { IPublicRoomsChunkRoom } from "../client.ts";
|
||||
import { RoomType } from "./event.ts";
|
||||
import { IStrippedState } from "../sync-accumulator.ts";
|
||||
|
||||
// Types relating to Rooms of type `m.space` and related APIs
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { RoomType } from "./event";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "./partials";
|
||||
import { ImageInfo } from "./media";
|
||||
import { PolicyRecommendation } from "../models/invites-ignorer";
|
||||
import { RoomType } from "./event.ts";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "./partials.ts";
|
||||
import { ImageInfo } from "./media.ts";
|
||||
import { PolicyRecommendation } from "../models/invites-ignorer.ts";
|
||||
|
||||
export interface RoomCanonicalAliasEventContent {
|
||||
alias?: string;
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IdServerUnbindResult } from "./partials";
|
||||
import { IdServerUnbindResult } from "./partials.ts";
|
||||
|
||||
// Types relating to Synapse Admin APIs
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
||||
import { ServerControlledNamespacedValue } from "../NamespacedValue.ts";
|
||||
|
||||
/**
|
||||
* https://github.com/matrix-org/matrix-doc/pull/3773
|
||||
|
||||
+2
-2
@@ -16,8 +16,8 @@ limitations under the License.
|
||||
|
||||
import { EitherAnd } from "matrix-events-sdk";
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IMessageRendering } from "./extensible_events";
|
||||
import { UnstableValue } from "../NamespacedValue.ts";
|
||||
import { IMessageRendering } from "./extensible_events.ts";
|
||||
|
||||
/**
|
||||
* Extensible topic event type based on MSC3765
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { AuthDict, IAuthData } from "../interactive-auth";
|
||||
import { AuthDict, IAuthData } from "../interactive-auth.ts";
|
||||
|
||||
/**
|
||||
* Helper type to represent HTTP request body for a UIA enabled endpoint
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter.ts";
|
||||
|
||||
export class ReEmitter {
|
||||
public constructor(private readonly target: EventEmitter) {}
|
||||
|
||||
@@ -14,14 +14,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ToDeviceMessageId } from "./@types/event";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixClient, ClientEvent } from "./client";
|
||||
import { MatrixError } from "./http-api";
|
||||
import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage";
|
||||
import { MatrixScheduler } from "./scheduler";
|
||||
import { SyncState } from "./sync";
|
||||
import { MapWithDefault } from "./utils";
|
||||
import { ToDeviceMessageId } from "./@types/event.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
import { MatrixClient, ClientEvent } from "./client.ts";
|
||||
import { MatrixError } from "./http-api/index.ts";
|
||||
import {
|
||||
IndexedToDeviceBatch,
|
||||
ToDeviceBatch,
|
||||
ToDeviceBatchWithTxnId,
|
||||
ToDevicePayload,
|
||||
} from "./models/ToDeviceMessage.ts";
|
||||
import { MatrixScheduler } from "./scheduler.ts";
|
||||
import { SyncState } from "./sync.ts";
|
||||
import { MapWithDefault } from "./utils.ts";
|
||||
|
||||
const MAX_BATCH_SIZE = 20;
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IClientWellKnown, IWellKnownConfig, IServerVersions } from "./client";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixError, Method, timeoutSignal } from "./http-api";
|
||||
import { SUPPORTED_MATRIX_VERSIONS } from "./version-support";
|
||||
import { IClientWellKnown, IWellKnownConfig, IServerVersions } from "./client.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
import { MatrixError, Method, timeoutSignal } from "./http-api/index.ts";
|
||||
import { SUPPORTED_MATRIX_VERSIONS } from "./version-support.ts";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
import * as matrixcs from "./matrix.ts";
|
||||
|
||||
type BrowserMatrix = typeof matrixcs;
|
||||
declare global {
|
||||
@@ -33,12 +33,12 @@ globalThis.__js_sdk_entrypoint = true;
|
||||
let indexedDB: IDBFactory | undefined;
|
||||
try {
|
||||
indexedDB = globalThis.indexedDB;
|
||||
} catch (e) {}
|
||||
} catch {}
|
||||
|
||||
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
|
||||
if (indexedDB) {
|
||||
matrixcs.setCryptoStoreFactory(() => new matrixcs.IndexedDBCryptoStore(indexedDB!, "matrix-js-sdk:crypto"));
|
||||
}
|
||||
|
||||
export * from "./matrix";
|
||||
export * from "./matrix.ts";
|
||||
globalThis.matrixcs = matrixcs;
|
||||
|
||||
+629
-235
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user