Compare commits
217 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 | |||
| 6520e0f54f | |||
| ed7b314e6a | |||
| 24eff501e4 | |||
| 51544f25a7 | |||
| a0d73dfaca | |||
| 5d2500b7a7 | |||
| eff52b82e8 | |||
| 2868308079 | |||
| a5ef569717 | |||
| c06b22ae7c | |||
| 712ba617de | |||
| 957329b218 | |||
| 1733ec7b7f | |||
| 03ed4f5dd7 | |||
| 6e641a28c0 | |||
| b36682cb99 | |||
| ccc2fb5663 | |||
| 0a7f7efd9d | |||
| ae58d0c8eb | |||
| 20a6704497 | |||
| 3337bda752 | |||
| d90292bff5 | |||
| 3de0c02757 | |||
| 65b9c31f9b | |||
| d629a685c2 | |||
| 0210106be2 | |||
| 3e05a71068 | |||
| c755810d9c | |||
| 2d492f60a0 | |||
| 16db2c5f9a | |||
| 29c02d8c37 | |||
| 0f98df158c | |||
| 2ea4ce0bb6 | |||
| 3e0017fecf | |||
| a0073ddaaf | |||
| c29e116c0c | |||
| d9f372ca79 | |||
| 6417f4fac7 | |||
| 4bae83f59f | |||
| b8c68eb102 | |||
| 8790cde6d4 |
@@ -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 }}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
- name: "A-Element-R"
|
||||
description: "Issues affecting the port of Element's crypto layer to Rust"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Packaging"
|
||||
description: "Packaging, signing, releasing"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Technical-Debt"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Testing"
|
||||
description: "Testing, code coverage, etc."
|
||||
color: "bfd4f2"
|
||||
- name: "backport staging"
|
||||
description: "Label to automatically backport PR to staging branch"
|
||||
color: "B60205"
|
||||
- name: "Dependencies"
|
||||
description: "Pull requests that update a dependency file"
|
||||
color: "0366d6"
|
||||
- name: "Easy"
|
||||
color: "5dc9f7"
|
||||
- name: "Sponsored"
|
||||
color: "ffc8f4"
|
||||
- name: "T-Deprecation"
|
||||
description: "A pull request that makes something deprecated"
|
||||
color: "98e6ae"
|
||||
- name: "T-Other"
|
||||
description: "Questions, user support, anything else"
|
||||
color: "98e6ae"
|
||||
- name: "X-Blocked"
|
||||
color: "ff7979"
|
||||
- name: "X-Breaking-Change"
|
||||
color: "ff7979"
|
||||
- name: "X-Reverted"
|
||||
description: "PR has been reverted"
|
||||
color: "F68AA3"
|
||||
- name: "X-Upcoming-Release-Blocker"
|
||||
description: "This does not affect the current release cycle but will affect the next one"
|
||||
color: "e99695"
|
||||
- name: "Z-Community-PR"
|
||||
description: "Issue is solved by a community member's PR"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Test"
|
||||
description: "A test is raising false alarms"
|
||||
color: "ededed"
|
||||
@@ -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
|
||||
|
||||
@@ -24,10 +24,6 @@ on:
|
||||
description: List of github projects (owner/repo) which should have their dependency bumped to the newly released version (in JSON string array string syntax)
|
||||
type: string
|
||||
required: false
|
||||
include-changes:
|
||||
description: Project to include changelog entries from in this release.
|
||||
type: string
|
||||
required: false
|
||||
gpg-fingerprint:
|
||||
description: Fingerprint of the GPG key to use for signing the git tag and assets, if any.
|
||||
type: string
|
||||
@@ -45,7 +41,7 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
environment: Release
|
||||
steps:
|
||||
- name: Load GPG key
|
||||
@@ -120,6 +116,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
@@ -279,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
|
||||
@@ -307,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:
|
||||
@@ -319,6 +316,11 @@ jobs:
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.npm.outputs.id }}
|
||||
|
||||
@@ -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:
|
||||
@@ -25,6 +25,7 @@ jobs:
|
||||
with:
|
||||
cache: "yarn"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
@@ -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
|
||||
@@ -43,6 +43,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
@@ -59,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@a5ac7e51b41094c92402da3b24376905380afc29 # 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,13 +11,14 @@ concurrency:
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
@@ -37,13 +38,14 @@ jobs:
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
@@ -53,13 +55,14 @@ jobs:
|
||||
|
||||
workflow_lint:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
@@ -69,19 +72,20 @@ jobs:
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc --treatWarningsAsErrors"
|
||||
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -93,13 +97,14 @@ jobs:
|
||||
|
||||
analyse_dead_code:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Sync labels
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
schedule:
|
||||
- cron: "0 1 * * *" # 1am every day
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- .github/labels.yml
|
||||
jobs:
|
||||
sync-labels:
|
||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
|
||||
with:
|
||||
LABELS: |
|
||||
element-hq/element-meta
|
||||
.github/labels.yml
|
||||
DELETE: true
|
||||
WET: true
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
@@ -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: [18, "lts/*", 21]
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
module.exports = {
|
||||
sourceMaps: true,
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: {
|
||||
esmodules: true,
|
||||
},
|
||||
// We want to output ES modules for the final build (mostly to ensure that
|
||||
// async imports work correctly). However, jest doesn't support ES modules very
|
||||
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
|
||||
// when testing.
|
||||
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"@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",
|
||||
"@babel/plugin-transform-class-properties",
|
||||
"@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",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
module.exports = {
|
||||
sourceMaps: true,
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: {
|
||||
esmodules: true,
|
||||
},
|
||||
// We want to output ES modules for the final build (mostly to ensure that
|
||||
// async imports work correctly). However, jest doesn't support ES modules very
|
||||
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
|
||||
// when testing.
|
||||
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
|
||||
},
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
plugins: [
|
||||
"@babel/plugin-transform-numeric-separator",
|
||||
"@babel/plugin-transform-class-properties",
|
||||
"@babel/plugin-transform-object-rest-spread",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime",
|
||||
],
|
||||
};
|
||||
+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 });
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"preinstall": "npm install ../.."
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache 2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"cli-color": "^1.0.0"
|
||||
"cli-color": "^1.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;
|
||||
}
|
||||
|
||||
+25
-26
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "34.0.0",
|
||||
"version": "34.10.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "yarn build",
|
||||
@@ -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,14 +50,15 @@
|
||||
],
|
||||
"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": "^5.0.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"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",
|
||||
@@ -82,31 +80,33 @@
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"@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",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "9",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"@types/uuid": "10",
|
||||
"@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": "^53.0.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "9.11.0",
|
||||
"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.25.10",
|
||||
"typedoc": "^0.26.0",
|
||||
"typedoc-plugin-coverage": "^3.0.0",
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
"typedoc-plugin-missing-exports": "^2.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
"typedoc-plugin-missing-exports": "^3.0.0",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,14 @@ const main = async ({ github, releaseId, dependencies }) => {
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [owner, repo] = GITHUB_REPOSITORY.split("/");
|
||||
|
||||
const { data: release } = await github.rest.repos.getRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
const sections = Object.fromEntries(categories.map((cat) => [cat, []]));
|
||||
parseReleaseNotes(release.body, sections);
|
||||
for (const dependency of dependencies) {
|
||||
const releases = await getReleases(github, dependency);
|
||||
for (const release of releases) {
|
||||
@@ -120,12 +127,6 @@ const main = async ({ github, releaseId, dependencies }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: release } = await github.rest.repos.getRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
const intro = release.body.split(HEADING_PREFIX, 2)[0].trim();
|
||||
|
||||
let output = "";
|
||||
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,17 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fsProm = require("fs/promises");
|
||||
|
||||
const PKGJSON = "package.json";
|
||||
|
||||
async function main() {
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
|
||||
for (const field of ["main", "typings"]) {
|
||||
if (pkgJson["matrix_lib_" + field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_" + field];
|
||||
}
|
||||
}
|
||||
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
+1
@@ -0,0 +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",
|
||||
|
||||
@@ -19,7 +19,7 @@ import anotherjson from "another-json";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { MockResponse, MockResponseFunction } from "fetch-mock";
|
||||
import FetchMock from "fetch-mock";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
@@ -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";
|
||||
@@ -157,7 +160,7 @@ async function expectSendRoomKey(
|
||||
return await new Promise<Olm.InboundGroupSession>((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp("/sendToDevice/m.room.encrypted/"),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
const content = JSON.parse(opts.body as string);
|
||||
resolve(onSendRoomKey(content));
|
||||
return {};
|
||||
@@ -291,7 +294,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
|
||||
* (or a function that returns one).
|
||||
*/
|
||||
function expectAliceKeyClaim(response: MockResponse | MockResponseFunction) {
|
||||
function expectAliceKeyClaim(response: FetchMock.MockResponse | FetchMock.MockResponseFunction) {
|
||||
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
||||
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -1419,7 +1511,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
fetchMock.putOnce(
|
||||
{ url: new RegExp("/send/"), name: "send-event" },
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
const content = JSON.parse(opts.body as string);
|
||||
logger.log("/send:", content);
|
||||
// make sure that a new session is used
|
||||
@@ -1484,7 +1576,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
// mark the device as known, and resend.
|
||||
aliceClient.setDeviceKnown(aliceClient.getUserId()!, "DEVICE_ID");
|
||||
expectAliceKeyClaim((url: string, opts: RequestInit): MockResponse => {
|
||||
expectAliceKeyClaim((url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
const content = JSON.parse(opts.body as string);
|
||||
expect(content.one_time_keys[aliceClient.getUserId()!].DEVICE_ID).toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceClient.getUserId()!);
|
||||
@@ -2180,11 +2272,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
|
||||
// ... and finally, send the room key. We block the response until `sendRoomMessageDefer` completes.
|
||||
const sendRoomMessageDefer = defer<MockResponse>();
|
||||
const sendRoomMessageDefer = defer<FetchMock.MockResponse>();
|
||||
const reqProm = new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp("/send/m.room.encrypted/"),
|
||||
async (url: string, opts: RequestInit): Promise<MockResponse> => {
|
||||
async (url: string, opts: RequestInit): Promise<FetchMock.MockResponse> => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
return await sendRoomMessageDefer.promise;
|
||||
},
|
||||
@@ -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: [
|
||||
|
||||
@@ -66,26 +66,6 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
expect(databaseNames).toEqual(expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto"]));
|
||||
});
|
||||
|
||||
it("should create the meta db if given a pickleKey", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
pickleKey: "testKey",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
// should have two indexed dbs now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(
|
||||
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should create the meta db if given a storageKey", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
@@ -470,10 +450,9 @@ describe("MatrixClient.clearStores", () => {
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
pickleKey: "testKey",
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
await matrixClient.initRustCrypto({ storagePassword: "testKey" });
|
||||
expect(await indexedDB.databases()).toHaveLength(2);
|
||||
await matrixClient.stopClient();
|
||||
|
||||
|
||||
@@ -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?
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
import FetchMock from "fetch-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { createHash } from "crypto";
|
||||
@@ -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);
|
||||
@@ -1511,7 +1512,7 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
|
||||
return new Promise((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
return {};
|
||||
},
|
||||
@@ -1535,7 +1536,7 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
|
||||
|
||||
fetchMock.put(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
|
||||
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
|
||||
const content = Object.values(messages)[0] as any;
|
||||
|
||||
@@ -2281,67 +2281,70 @@ describe("MatrixClient syncing", () => {
|
||||
httpBackend!.expectedRequests = [];
|
||||
});
|
||||
|
||||
it("should return a room based on the room initialSync API", async () => {
|
||||
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, {
|
||||
room_id: roomOne,
|
||||
membership: KnownMembership.Leave,
|
||||
messages: {
|
||||
start: "start",
|
||||
end: "end",
|
||||
chunk: [
|
||||
it.each([undefined, 100])(
|
||||
"should return a room based on the room initialSync API with limit %s",
|
||||
async (limit) => {
|
||||
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, {
|
||||
room_id: roomOne,
|
||||
membership: KnownMembership.Leave,
|
||||
messages: {
|
||||
start: "start",
|
||||
end: "end",
|
||||
chunk: [
|
||||
{
|
||||
content: { body: "Message 1" },
|
||||
type: "m.room.message",
|
||||
event_id: "$eventId1",
|
||||
sender: userA,
|
||||
origin_server_ts: 12313525,
|
||||
room_id: roomOne,
|
||||
},
|
||||
{
|
||||
content: { body: "Message 2" },
|
||||
type: "m.room.message",
|
||||
event_id: "$eventId2",
|
||||
sender: userB,
|
||||
origin_server_ts: 12315625,
|
||||
room_id: roomOne,
|
||||
},
|
||||
],
|
||||
},
|
||||
state: [
|
||||
{
|
||||
content: { body: "Message 1" },
|
||||
type: "m.room.message",
|
||||
event_id: "$eventId1",
|
||||
content: { name: "Room Name" },
|
||||
type: "m.room.name",
|
||||
event_id: "$eventId",
|
||||
sender: userA,
|
||||
origin_server_ts: 12313525,
|
||||
room_id: roomOne,
|
||||
},
|
||||
{
|
||||
content: { body: "Message 2" },
|
||||
type: "m.room.message",
|
||||
event_id: "$eventId2",
|
||||
sender: userB,
|
||||
origin_server_ts: 12315625,
|
||||
origin_server_ts: 12314525,
|
||||
state_key: "",
|
||||
room_id: roomOne,
|
||||
},
|
||||
],
|
||||
},
|
||||
state: [
|
||||
{
|
||||
content: { name: "Room Name" },
|
||||
type: "m.room.name",
|
||||
event_id: "$eventId",
|
||||
sender: userA,
|
||||
origin_server_ts: 12314525,
|
||||
state_key: "",
|
||||
room_id: roomOne,
|
||||
},
|
||||
],
|
||||
presence: [
|
||||
{
|
||||
content: {},
|
||||
type: "m.presence",
|
||||
sender: userA,
|
||||
},
|
||||
],
|
||||
});
|
||||
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
|
||||
presence: [
|
||||
{
|
||||
content: {},
|
||||
type: "m.presence",
|
||||
sender: userA,
|
||||
},
|
||||
],
|
||||
});
|
||||
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
|
||||
|
||||
const prom = client!.peekInRoom(roomOne);
|
||||
await httpBackend!.flushAllExpected();
|
||||
const room = await prom;
|
||||
const prom = client!.peekInRoom(roomOne, limit);
|
||||
await httpBackend!.flushAllExpected();
|
||||
const room = await prom;
|
||||
|
||||
expect(room.roomId).toBe(roomOne);
|
||||
expect(room.getMyMembership()).toBe(KnownMembership.Leave);
|
||||
expect(room.name).toBe("Room Name");
|
||||
expect(room.currentState.getStateEvents("m.room.name", "")?.getId()).toBe("$eventId");
|
||||
expect(room.timeline[0].getContent().body).toBe("Message 1");
|
||||
expect(room.timeline[1].getContent().body).toBe("Message 2");
|
||||
client?.stopPeeking();
|
||||
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
|
||||
await httpBackend!.flushAllExpected();
|
||||
});
|
||||
expect(room.roomId).toBe(roomOne);
|
||||
expect(room.getMyMembership()).toBe(KnownMembership.Leave);
|
||||
expect(room.name).toBe("Room Name");
|
||||
expect(room.currentState.getStateEvents("m.room.name", "")?.getId()).toBe("$eventId");
|
||||
expect(room.timeline[0].getContent().body).toBe("Message 1");
|
||||
expect(room.timeline[1].getContent().body).toBe("Message 2");
|
||||
client?.stopPeeking();
|
||||
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
|
||||
await httpBackend!.flushAllExpected();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("user account data", () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -15,7 +15,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { MockOptionsMethodPut } from "fetch-mock";
|
||||
|
||||
import { ISyncResponder } from "./SyncResponder";
|
||||
|
||||
@@ -28,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.
|
||||
@@ -40,7 +39,10 @@ export class AccountDataAccumulator {
|
||||
* @param opts - options to pass to fetchMock
|
||||
* @returns a Promise which will resolve (with the content of the account data) once it is set.
|
||||
*/
|
||||
public interceptSetAccountData(accountDataType: string, opts?: MockOptionsMethodPut): Promise<any> {
|
||||
public interceptSetAccountData(
|
||||
accountDataType: string,
|
||||
opts?: Parameters<(typeof fetchMock)["put"]>[2],
|
||||
): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
// Called when the cross signing key is uploaded
|
||||
fetchMock.put(
|
||||
@@ -99,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.
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import debugFunc from "debug";
|
||||
import { Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
import FetchMock from "fetch-mock";
|
||||
|
||||
/** Interface implemented by classes that intercept `/sync` requests from test clients
|
||||
*
|
||||
@@ -80,7 +80,7 @@ export class SyncResponder implements ISyncResponder {
|
||||
);
|
||||
}
|
||||
|
||||
private async onSyncRequest(): Promise<MockResponse> {
|
||||
private async onSyncRequest(): Promise<FetchMock.MockResponse> {
|
||||
switch (this.state) {
|
||||
case SyncResponderState.IDLE: {
|
||||
this.debug("Got /sync request: waiting for response to be ready");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -26,7 +26,6 @@ import { getRandomValues } from "node:crypto";
|
||||
import { TextEncoder } from "node:util";
|
||||
|
||||
import { Method } from "../../../src";
|
||||
import * as crypto from "../../../src/crypto/crypto";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
completeAuthorizationCodeGrant,
|
||||
@@ -39,11 +38,6 @@ import { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../test-uti
|
||||
|
||||
jest.mock("jwt-decode");
|
||||
|
||||
const webCrypto = new Crypto();
|
||||
|
||||
// save for resetting mocks
|
||||
const realSubtleCrypto = crypto.subtleCrypto;
|
||||
|
||||
describe("oidc authorization", () => {
|
||||
const delegatedAuthConfig = makeDelegatedAuthConfig();
|
||||
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
|
||||
@@ -62,7 +56,11 @@ describe("oidc authorization", () => {
|
||||
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
|
||||
mockOpenIdConfiguration(),
|
||||
);
|
||||
global.TextEncoder = TextEncoder;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const webCrypto = new Crypto();
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: {
|
||||
getRandomValues,
|
||||
@@ -70,12 +68,6 @@ describe("oidc authorization", () => {
|
||||
subtle: webCrypto.subtle,
|
||||
},
|
||||
});
|
||||
global.TextEncoder = TextEncoder;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// @ts-ignore reset any ugly mocking we did
|
||||
crypto.subtleCrypto = realSubtleCrypto;
|
||||
});
|
||||
|
||||
it("should generate authorization params", () => {
|
||||
@@ -97,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
|
||||
crypto.subtleCrypto = undefined;
|
||||
|
||||
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
|
||||
authorizationParams.codeVerifier = "test-code-verifier";
|
||||
const authUrl = new URL(
|
||||
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
|
||||
);
|
||||
@@ -113,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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1122,4 +1122,59 @@ describe("RoomState", function () {
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
describe("skipWrongOrderRoomStateInserts", () => {
|
||||
const idNow = "now";
|
||||
const idBefore = "before";
|
||||
const endState = new RoomState(roomId);
|
||||
const startState = new RoomState(roomId, undefined, true);
|
||||
|
||||
let onRoomStateEvent: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
|
||||
const evNow = new MatrixEvent({
|
||||
type: "m.call.member",
|
||||
room_id: roomId,
|
||||
state_key: "@user:example.org",
|
||||
content: {},
|
||||
});
|
||||
evNow.event.unsigned = { replaces_state: idBefore };
|
||||
evNow.event.event_id = idNow;
|
||||
|
||||
const evBefore = new MatrixEvent({
|
||||
type: "m.call.member",
|
||||
room_id: roomId,
|
||||
state_key: "@user:example.org",
|
||||
content: {},
|
||||
});
|
||||
|
||||
const updatedStateWithBefore = jest.fn();
|
||||
const updatedStateWithNow = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
evBefore.event.event_id = idBefore;
|
||||
onRoomStateEvent = (event, _state, _lastState) => {
|
||||
if (event.event.event_id === idNow) {
|
||||
updatedStateWithNow();
|
||||
} else if (event.event.event_id === idBefore) {
|
||||
updatedStateWithBefore();
|
||||
}
|
||||
};
|
||||
endState.on(RoomStateEvent.Events, onRoomStateEvent);
|
||||
startState.on(RoomStateEvent.Events, onRoomStateEvent);
|
||||
});
|
||||
afterEach(() => {
|
||||
endState.off(RoomStateEvent.Events, onRoomStateEvent);
|
||||
startState.off(RoomStateEvent.Events, onRoomStateEvent);
|
||||
updatedStateWithNow.mockReset();
|
||||
updatedStateWithBefore.mockReset();
|
||||
});
|
||||
it("should skip inserting state to the end of the timeline if the current endState events replaces_state id is the same as the inserted events id", () => {
|
||||
endState.setStateEvents([evNow, evBefore]);
|
||||
expect(updatedStateWithBefore).not.toHaveBeenCalled();
|
||||
expect(updatedStateWithNow).toHaveBeenCalled();
|
||||
});
|
||||
it("should skip inserting state at the beginning of the timeline if the inserted events replaces_state is the event id of the current startState", () => {
|
||||
startState.setStateEvents([evBefore, evNow]);
|
||||
expect(updatedStateWithBefore).toHaveBeenCalled();
|
||||
expect(updatedStateWithNow).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user