Compare commits
91 Commits
v41.1.0-rc.0
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 14b2ee2da4 | |||
| bb083222d9 | |||
| fa424c44b4 | |||
| fef093747e | |||
| 4b33892d48 | |||
| d7d771fadb | |||
| 4ee3e591bf | |||
| 668183d722 | |||
| 854dae0dc0 | |||
| 9d1aca2232 | |||
| 81569f3461 | |||
| 50783aba76 | |||
| fd01b17236 | |||
| 25e92009b7 | |||
| eb7acfb810 | |||
| ca5655bced | |||
| ef9b13e2a6 | |||
| 159cca0363 | |||
| 3879111850 | |||
| f9a5aa87e3 | |||
| ed58df040c | |||
| 0a3448d4c9 | |||
| 25c1c1ea26 | |||
| a5e67af31f | |||
| b91e80814a | |||
| d096a72605 | |||
| fb547e7b4b | |||
| 815294ca5a | |||
| 6d270b4685 | |||
| 8bc3d96f6b | |||
| b6ea6e105e | |||
| cd4e053fa5 | |||
| 727473af62 | |||
| f17f013f1e | |||
| 9f4ab0b840 | |||
| 6371e4b252 | |||
| b69929e01a | |||
| 9dc12baaa9 | |||
| 159738597d | |||
| d02205652f | |||
| 5e03add29a | |||
| eeafd7fcaa | |||
| 78a3c5372d | |||
| c0c3bc2a8c | |||
| c3ce49cabf | |||
| 5408168dfd | |||
| 61452ddc11 | |||
| d5160a5380 | |||
| 7ff4960a27 | |||
| 00f63db80f | |||
| 9bcb83a20a | |||
| dd8d8e5410 | |||
| 32b8ff8116 | |||
| 3ceadd512d | |||
| 8182180550 | |||
| aed74c5a72 | |||
| 8c259c53a6 | |||
| 4d59291538 | |||
| 80009a1b31 | |||
| 93e6c95953 | |||
| 27a5507cef | |||
| be06f6655e | |||
| 71152f33bf | |||
| bc8f67089c | |||
| acc9aa8939 | |||
| e76f627fe3 | |||
| 45b1e73842 | |||
| f3eefd2f32 | |||
| f7c053216b | |||
| 9c7739f14f | |||
| 897afe153a | |||
| a929391dcd | |||
| 45c5ee9f65 | |||
| e56aaa16c7 | |||
| da0d3d791e | |||
| ed5eb670a1 | |||
| b7fcb6e4c1 | |||
| bd775f6b61 | |||
| c2f9ad28fc | |||
| 7f33e3462e | |||
| d99363d288 | |||
| 3642b99212 | |||
| 6ec0987286 | |||
| 219eb617dc | |||
| c6f9b25046 | |||
| 5d0e2efaf3 | |||
| c7cd5570d3 | |||
| c2f6dd2ce0 | |||
| 393732aaae | |||
| d373fd8540 | |||
| 44a8a9a47a |
@@ -22,7 +22,7 @@ runs:
|
|||||||
|
|
||||||
- name: Upload tarball signature
|
- name: Upload tarball signature
|
||||||
if: ${{ inputs.upload-url }}
|
if: ${{ inputs.upload-url }}
|
||||||
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
|
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ inputs.upload-url }}
|
upload_url: ${{ inputs.upload-url }}
|
||||||
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ runs:
|
|||||||
|
|
||||||
- name: Upload asset signatures
|
- name: Upload asset signatures
|
||||||
if: inputs.gpg-fingerprint
|
if: inputs.gpg-fingerprint
|
||||||
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
|
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ inputs.upload-url }}
|
upload_url: ${{ inputs.upload-url }}
|
||||||
asset_path: ${{ inputs.asset-path }}.asc
|
asset_path: ${{ inputs.asset-path }}.asc
|
||||||
|
|
||||||
- name: Upload assets
|
- name: Upload assets
|
||||||
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
|
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ inputs.upload-url }}
|
upload_url: ${{ inputs.upload-url }}
|
||||||
asset_path: ${{ inputs.asset-path }}
|
asset_path: ${{ inputs.asset-path }}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
name: Backport
|
name: Backport
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
# Privilege escalation necessary to enable backporting PRs from forks
|
||||||
|
# 🚨 We must not execute any checked out code here.
|
||||||
|
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||||
types:
|
types:
|
||||||
- closed
|
- closed
|
||||||
- labeled
|
- labeled
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
name: Deploy documentation PR preview
|
name: Deploy documentation PR preview
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
# Privilege escalation necessary to publish to Netlify
|
||||||
|
# 🚨 We must not execute any checked out code here.
|
||||||
|
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||||
workflows: ["Static Analysis"]
|
workflows: ["Static Analysis"]
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
@@ -15,7 +17,7 @@ jobs:
|
|||||||
deployments: write
|
deployments: write
|
||||||
steps:
|
steps:
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Triggers after the "Downstream artifacts" build has finished, to run the
|
# Triggers after the "Downstream artifacts" build has finished, to run the
|
||||||
# matrix-react-sdk playwright tests (with access to repo secrets)
|
# element-web playwright tests (with access to repo secrets)
|
||||||
|
|
||||||
name: matrix-react-sdk End to End Tests
|
name: Element Web End to End Tests
|
||||||
on:
|
on:
|
||||||
merge_group:
|
merge_group:
|
||||||
types: [checks_requested]
|
types: [checks_requested]
|
||||||
@@ -21,11 +21,12 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
playwright:
|
playwright:
|
||||||
name: Playwright
|
name: Playwright
|
||||||
uses: element-hq/element-web/.github/workflows/end-to-end-tests.yaml@develop
|
uses: element-hq/element-web/.github/workflows/build-and-test.yaml@develop # zizmor: ignore[unpinned-uses]
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
issues: read
|
issues: read
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
|
contents: read
|
||||||
with:
|
with:
|
||||||
matrix-js-sdk-sha: ${{ github.sha }}
|
matrix-js-sdk-sha: ${{ github.sha }}
|
||||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
- name: Notify element-web repo that a new SDK build is on develop so it can CI against it
|
||||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
|
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
name: Pull Request
|
name: Pull Request
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
# Privilege escalation necessary access members of the review teams
|
||||||
|
# 🚨 We must not execute any checked out code here, and be careful around use of user-controlled inputs.
|
||||||
|
# FIXME: only `community-prs` job needs this privilege, so it should be in its own workflow file.
|
||||||
|
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||||
merge_group:
|
merge_group:
|
||||||
types: [checks_requested]
|
types: [checks_requested]
|
||||||
@@ -15,7 +18,7 @@ jobs:
|
|||||||
name: Preview Changelog
|
name: Preview Changelog
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5
|
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
|
||||||
if: github.event_name != 'merge_group'
|
if: github.event_name != 'merge_group'
|
||||||
with:
|
with:
|
||||||
labels: |
|
labels: |
|
||||||
@@ -35,7 +38,7 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
steps:
|
steps:
|
||||||
- name: Add notice
|
- name: Add notice
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
@@ -60,7 +63,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add label
|
- name: Add label
|
||||||
if: steps.teams.outputs.isTeamMember == 'false'
|
if: steps.teams.outputs.isTeamMember == 'false'
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.addLabels({
|
github.rest.issues.addLabels({
|
||||||
@@ -81,7 +84,7 @@ jobs:
|
|||||||
github.event.pull_request.head.repo.full_name != github.repository
|
github.event.pull_request.head.repo.full_name != github.repository
|
||||||
steps:
|
steps:
|
||||||
- name: Close pull request
|
- name: Close pull request
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.createComment({
|
github.rest.issues.createComment({
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check for X-Release-Blocker label on any open issues or PRs
|
- name: Check for X-Release-Blocker label on any open issues or PRs
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
REPO: ${{ inputs.repository }}
|
REPO: ${{ inputs.repository }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: staging
|
ref: staging
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version-file: package.json
|
node-version-file: package.json
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
@@ -49,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Ingest upstream changes
|
- name: Ingest upstream changes
|
||||||
if: inputs.include-changes
|
if: inputs.include-changes
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ jobs:
|
|||||||
draft:
|
draft:
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
|
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop # zizmor: ignore[unpinned-uses]
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ jobs:
|
|||||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Get actions scripts
|
- name: Get actions scripts
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
@@ -37,8 +38,8 @@ jobs:
|
|||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
scripts/release
|
scripts/release
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version-file: package.json
|
node-version-file: package.json
|
||||||
|
|||||||
@@ -33,10 +33,14 @@ on:
|
|||||||
description: The number of expected assets, including signatures, excluding generated zip & tarball.
|
description: The number of expected assets, including signatures, excluding generated zip & tarball.
|
||||||
type: number
|
type: number
|
||||||
required: false
|
required: false
|
||||||
dir:
|
dist-dir:
|
||||||
description: The directory to release
|
description: The directory to release
|
||||||
type: string
|
type: string
|
||||||
default: "."
|
default: "."
|
||||||
|
version-dirs:
|
||||||
|
description: Directories in which to update package.json `version` field
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
outputs:
|
outputs:
|
||||||
npm-id:
|
npm-id:
|
||||||
description: "The npm package@version string we published"
|
description: "The npm package@version string we published"
|
||||||
@@ -48,7 +52,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
issues: read
|
issues: read
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
|
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop # zizmor: ignore[unpinned-uses]
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
@@ -61,7 +65,7 @@ jobs:
|
|||||||
- name: Load GPG key
|
- name: Load GPG key
|
||||||
id: gpg
|
id: gpg
|
||||||
if: inputs.gpg-fingerprint
|
if: inputs.gpg-fingerprint
|
||||||
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6
|
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
@@ -69,7 +73,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get draft release
|
- name: Get draft release
|
||||||
id: draft-release
|
id: draft-release
|
||||||
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1
|
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
with:
|
with:
|
||||||
@@ -82,6 +86,7 @@ jobs:
|
|||||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Get actions scripts
|
- name: Get actions scripts
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
@@ -95,7 +100,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Prepare variables
|
- name: Prepare variables
|
||||||
id: prepare
|
id: prepare
|
||||||
working-directory: ${{ inputs.dir }}
|
working-directory: ${{ inputs.dist-dir }}
|
||||||
run: |
|
run: |
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
@@ -110,7 +115,7 @@ jobs:
|
|||||||
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
|
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Check version number not in use
|
- name: Check version number not in use
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { VERSION } = process.env;
|
const { VERSION } = process.env;
|
||||||
@@ -129,17 +134,17 @@ jobs:
|
|||||||
git config --global user.email "releases@riot.im"
|
git config --global user.email "releases@riot.im"
|
||||||
git config --global user.name "RiotRobot"
|
git config --global user.name "RiotRobot"
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version-file: ${{ inputs.dir }}/package.json
|
node-version-file: ${{ inputs.dist-dir }}/package.json
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: "pnpm install --frozen-lockfile"
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
- name: Handle develop dependencies
|
- name: Handle develop dependencies
|
||||||
working-directory: ${{ inputs.dir }}
|
working-directory: ${{ inputs.dist-dir }}
|
||||||
run: |
|
run: |
|
||||||
ret=0
|
ret=0
|
||||||
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
|
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
|
||||||
@@ -153,11 +158,14 @@ jobs:
|
|||||||
git commit -m "Keep $PACKAGE at $VERSION"
|
git commit -m "Keep $PACKAGE at $VERSION"
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Bump package.json version
|
- name: Bump package.json versions
|
||||||
working-directory: ${{ inputs.dir }}
|
|
||||||
run: |
|
run: |
|
||||||
pnpm version --no-git-tag-version "${VERSION#v}"
|
for DIR in $DIRS; do
|
||||||
git add package.json
|
pnpm version -C "$DIR" --no-git-tag-version "${VERSION#v}"
|
||||||
|
git add "$DIR"/package.json
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
DIRS: ${{ inputs.version-dirs || inputs.dist-dir }}
|
||||||
|
|
||||||
- name: Add to CHANGELOG.md
|
- name: Add to CHANGELOG.md
|
||||||
if: inputs.final
|
if: inputs.final
|
||||||
@@ -184,7 +192,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build assets
|
- name: Build assets
|
||||||
if: steps.prepare.outputs.has-dist-script == '1'
|
if: steps.prepare.outputs.has-dist-script == '1'
|
||||||
working-directory: ${{ inputs.dir }}
|
working-directory: ${{ inputs.dist-dir }}
|
||||||
run: DIST_VERSION="$VERSION" pnpm dist
|
run: DIST_VERSION="$VERSION" pnpm dist
|
||||||
|
|
||||||
- name: Upload release assets & signatures
|
- name: Upload release assets & signatures
|
||||||
@@ -193,7 +201,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
|
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
|
||||||
upload-url: ${{ steps.draft-release.outputs.upload_url }}
|
upload-url: ${{ steps.draft-release.outputs.upload_url }}
|
||||||
asset-path: ${{ inputs.dir }}/${{ inputs.asset-path }}
|
asset-path: ${{ inputs.dist-dir }}/${{ inputs.asset-path }}
|
||||||
|
|
||||||
- name: Create signed tag
|
- name: Create signed tag
|
||||||
if: inputs.gpg-fingerprint
|
if: inputs.gpg-fingerprint
|
||||||
@@ -226,7 +234,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Validate release has expected assets
|
- name: Validate release has expected assets
|
||||||
if: inputs.expected-asset-count
|
if: inputs.expected-asset-count
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||||
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
|
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
|
||||||
@@ -254,7 +262,7 @@ jobs:
|
|||||||
git push origin master
|
git push origin master
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||||
FINAL: ${{ inputs.final }}
|
FINAL: ${{ inputs.final }}
|
||||||
@@ -286,9 +294,9 @@ jobs:
|
|||||||
name: Publish to npm
|
name: Publish to npm
|
||||||
needs: release
|
needs: release
|
||||||
if: inputs.npm
|
if: inputs.npm
|
||||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
dir: ${{ inputs.dir }}
|
dir: ${{ inputs.dist-dir }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|||||||
@@ -25,10 +25,11 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
ref: staging
|
ref: staging
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- name: 🔧 pnpm cache
|
- name: 🔧 pnpm cache
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ concurrency: ${{ github.workflow }}
|
|||||||
permissions: {} # No permissions required
|
permissions: {} # No permissions required
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
|
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop # zizmor: ignore[unpinned-uses,secrets-inherit]
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
issues: write
|
issues: write
|
||||||
@@ -50,9 +50,10 @@ jobs:
|
|||||||
repository: ${{ matrix.repo }}
|
repository: ${{ matrix.repo }}
|
||||||
ref: staging
|
ref: staging
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -77,10 +78,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: 🧮 Checkout code
|
- name: 🧮 Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- name: 🔧 pnpm cache
|
- name: 🔧 pnpm cache
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version-file: package.json
|
node-version-file: package.json
|
||||||
@@ -92,7 +95,7 @@ jobs:
|
|||||||
run: pnpm gendoc
|
run: pnpm gendoc
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
|
||||||
with:
|
with:
|
||||||
path: _docs
|
path: _docs
|
||||||
|
|
||||||
@@ -110,4 +113,4 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
required: false
|
required: false
|
||||||
description: "Whether to combine multiple LCOV and sonar-report files in coverage artifact"
|
description: "Whether to combine multiple LCOV and sonar-report files in coverage artifact"
|
||||||
|
version-pkg-json-dir:
|
||||||
|
type: string
|
||||||
|
default: "."
|
||||||
|
description: "Relative path of the directory containing package.json with the `version` to use."
|
||||||
permissions: {}
|
permissions: {}
|
||||||
jobs:
|
jobs:
|
||||||
sonarqube:
|
sonarqube:
|
||||||
@@ -41,9 +45,10 @@ jobs:
|
|||||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
if: ${{ !inputs.sharded }}
|
if: ${{ !inputs.sharded }}
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -51,14 +56,13 @@ jobs:
|
|||||||
name: coverage
|
name: coverage
|
||||||
path: coverage
|
path: coverage
|
||||||
- name: 📥 Download sharded artifacts
|
- name: 📥 Download sharded artifacts
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
if: inputs.sharded
|
if: inputs.sharded
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
path: coverage
|
path: coverage
|
||||||
merge-multiple: true
|
|
||||||
- name: Check coverage artifact
|
- name: Check coverage artifact
|
||||||
run: |
|
run: |
|
||||||
if [ ! -d coverage ]; then
|
if [ ! -d coverage ]; then
|
||||||
@@ -75,7 +79,7 @@ jobs:
|
|||||||
|
|
||||||
- name: "🩻 SonarCloud Scan"
|
- name: "🩻 SonarCloud Scan"
|
||||||
id: sonarcloud
|
id: sonarcloud
|
||||||
uses: matrix-org/sonarcloud-workflow-action@ea0cd9dbd5562e79816685972bc0d03c235a900c
|
uses: matrix-org/sonarcloud-workflow-action@13968a27c924fa19b1dacbce6ca3ff217daa775b
|
||||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
# 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' }}
|
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||||
with:
|
with:
|
||||||
@@ -83,7 +87,7 @@ jobs:
|
|||||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||||
skip_coverage_label: Z-Skip-Coverage
|
skip_coverage_label: Z-Skip-Coverage
|
||||||
version_cmd: "cat package.json | jq -r .version"
|
version_cmd: "cat ${{ inputs.version-pkg-json-dir }}/package.json | jq -r .version"
|
||||||
branch: ${{ github.event.workflow_run.head_branch }}
|
branch: ${{ github.event.workflow_run.head_branch }}
|
||||||
revision: ${{ github.event.workflow_run.head_sha }}
|
revision: ${{ github.event.workflow_run.head_sha }}
|
||||||
token: ${{ secrets.SONAR_TOKEN }}
|
token: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
name: SonarQube
|
name: SonarQube
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
# Privilege escalation necessary to call upon SonarCloud
|
||||||
|
# 🚨 We must not execute any checked out code here.
|
||||||
|
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||||
workflows: ["Tests"]
|
workflows: ["Tests"]
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
@@ -16,7 +18,7 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
statuses: write
|
statuses: write
|
||||||
id-token: write # sonar
|
id-token: write # sonar
|
||||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop # zizmor: ignore[unpinned-uses]
|
||||||
secrets:
|
secrets:
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version-file: package.json
|
node-version-file: package.json
|
||||||
@@ -33,9 +35,11 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version-file: package.json
|
node-version-file: package.json
|
||||||
@@ -51,9 +55,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version-file: package.json
|
node-version-file: package.json
|
||||||
@@ -79,11 +85,15 @@ jobs:
|
|||||||
workflow_lint:
|
workflow_lint:
|
||||||
name: "Workflow Lint"
|
name: "Workflow Lint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version-file: package.json
|
node-version-file: package.json
|
||||||
@@ -94,14 +104,19 @@ jobs:
|
|||||||
- name: Run Linter
|
- name: Run Linter
|
||||||
run: "pnpm lint:workflows"
|
run: "pnpm lint:workflows"
|
||||||
|
|
||||||
|
- name: Run zizmor
|
||||||
|
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
name: "JSDoc Checker"
|
name: "JSDoc Checker"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version-file: package.json
|
node-version-file: package.json
|
||||||
@@ -113,7 +128,7 @@ jobs:
|
|||||||
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: docs
|
name: docs
|
||||||
path: _docs
|
path: _docs
|
||||||
@@ -125,9 +140,11 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version-file: package.json
|
node-version-file: package.json
|
||||||
@@ -146,9 +163,10 @@ jobs:
|
|||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -163,13 +181,19 @@ jobs:
|
|||||||
working-directory: apps/web
|
working-directory: apps/web
|
||||||
run: "pnpm run lint:types"
|
run: "pnpm run lint:types"
|
||||||
|
|
||||||
# Hook for branch protection to skip downstream typechecking outside of merge queues
|
# Workflow consolidation job
|
||||||
downstream:
|
done:
|
||||||
name: Downstream Typescript Syntax Check
|
needs:
|
||||||
|
- ts_lint
|
||||||
|
- js_lint
|
||||||
|
- node_example_lint
|
||||||
|
- workflow_lint
|
||||||
|
- docs
|
||||||
|
- analyse_dead_code
|
||||||
|
- element-web
|
||||||
|
name: Static Analysis
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
if: always()
|
if: always()
|
||||||
needs:
|
|
||||||
- element-web
|
|
||||||
steps:
|
steps:
|
||||||
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
|
- if: contains(needs.*.result , 'failure') || contains(needs.*.result, 'cancelled')
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ on:
|
|||||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||||
jobs:
|
jobs:
|
||||||
sync-labels:
|
sync-labels:
|
||||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
|
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@7f2f93fb9b52ece7a0998f60e64862aa203c1746
|
||||||
with:
|
with:
|
||||||
LABELS: |
|
LABELS: |
|
||||||
element-hq/element-meta
|
element-hq/element-meta
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
id: setupNode
|
id: setupNode
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
@@ -42,19 +44,22 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
pnpm test \
|
pnpm test \
|
||||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
--coverage=${ENABLE_COVERAGE} \
|
||||||
--maxWorkers ${{ steps.cpu-cores.outputs.count }} \
|
--maxWorkers ${NUM_WORKERS} \
|
||||||
./spec/${{ matrix.specs }}
|
./spec/${{ matrix.specs }}
|
||||||
env:
|
env:
|
||||||
SHARD: ${{ matrix.specs }}
|
SHARD: ${{ matrix.specs }}
|
||||||
|
NUM_WORKERS: ${{ steps.cpu-cores.outputs.count }}
|
||||||
|
|
||||||
- name: Move coverage files into place
|
- name: Move coverage files into place
|
||||||
if: env.ENABLE_COVERAGE == 'true'
|
if: env.ENABLE_COVERAGE == 'true'
|
||||||
run: mv coverage/lcov.info coverage/${{ steps.setupNode.outputs.node-version }}-${{ matrix.specs }}.lcov.info
|
run: mv coverage/lcov.info coverage/${NODE_VERSION}-${{ matrix.specs }}.lcov.info
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ steps.setupNode.outputs.node-version }}
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
if: env.ENABLE_COVERAGE == 'true'
|
if: env.ENABLE_COVERAGE == 'true'
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
|
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
|
||||||
path: |
|
path: |
|
||||||
@@ -74,7 +79,7 @@ jobs:
|
|||||||
element-web:
|
element-web:
|
||||||
name: Downstream test element-web
|
name: Downstream test element-web
|
||||||
if: github.event_name == 'merge_group'
|
if: github.event_name == 'merge_group'
|
||||||
uses: element-hq/element-web/.github/workflows/tests.yml@develop
|
uses: element-hq/element-web/.github/workflows/tests.yml@develop # zizmor: ignore[unpinned-uses]
|
||||||
permissions:
|
permissions:
|
||||||
statuses: write
|
statuses: write
|
||||||
with:
|
with:
|
||||||
@@ -84,8 +89,8 @@ jobs:
|
|||||||
complement-crypto:
|
complement-crypto:
|
||||||
name: "Run Complement Crypto tests"
|
name: "Run Complement Crypto tests"
|
||||||
if: github.event_name == 'merge_group'
|
if: github.event_name == 'merge_group'
|
||||||
permissions: read-all
|
permissions: read-all # zizmor: ignore[excessive-permissions]
|
||||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
|
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
use_js_sdk: "."
|
use_js_sdk: "."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
automate-project-columns-next:
|
automate-project-columns-next:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@main
|
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/120
|
project-url: https://github.com/orgs/element-hq/projects/120
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ on:
|
|||||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||||
jobs:
|
jobs:
|
||||||
call-triage-labelled:
|
call-triage-labelled:
|
||||||
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
|
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@6339bcda15c71d209303b18a06a9b1c021220bf9
|
||||||
secrets:
|
secrets:
|
||||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
@@ -1,3 +1,27 @@
|
|||||||
|
Changes in [41.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.3.0) (2026-04-07)
|
||||||
|
==================================================================================================
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* Rotate the current room key when we see a member leave ([#5231](https://github.com/matrix-org/matrix-js-sdk/pull/5231)). Contributed by @kaylendog.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [41.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.2.0) (2026-03-24)
|
||||||
|
==================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* Only share history if room history visibility is shared ([#5216](https://github.com/matrix-org/matrix-js-sdk/pull/5216)). Contributed by @kaylendog.
|
||||||
|
* History sharing: resume key-bundle import on restart ([#5214](https://github.com/matrix-org/matrix-js-sdk/pull/5214)). Contributed by @richvdh.
|
||||||
|
* Move `CryptoApi.shareRoomHistoryWithUser` to `CryptoBackend` ([#5218](https://github.com/matrix-org/matrix-js-sdk/pull/5218)). Contributed by @richvdh.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [41.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.1.0) (2026-03-10)
|
||||||
|
==================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* Throw a specific error when the backup decryption key does not match the public backup ([#5202](https://github.com/matrix-org/matrix-js-sdk/pull/5202)). Contributed by @andybalaam.
|
||||||
|
* Update getUrlPreview to use /\_matrix/client/v1/media/preview\_url ([#5191](https://github.com/matrix-org/matrix-js-sdk/pull/5191)). Contributed by @Half-Shot.
|
||||||
|
|
||||||
|
|
||||||
Changes in [41.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.0.0) (2026-02-24)
|
Changes in [41.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.0.0) (2026-02-24)
|
||||||
==================================================================================================
|
==================================================================================================
|
||||||
## 🚨 BREAKING CHANGES
|
## 🚨 BREAKING CHANGES
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { KnipConfig } from "knip";
|
import { KnipConfig } from "knip";
|
||||||
|
|
||||||
|
// Specify this as knip loads config files which may conditionally add reporters, e.g. `vitest-sonar-reporter'
|
||||||
|
process.env.GITHUB_ACTIONS = "1";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
entry: [
|
entry: [
|
||||||
"src/index.ts",
|
"src/index.ts",
|
||||||
@@ -28,10 +31,6 @@ export default {
|
|||||||
"husky",
|
"husky",
|
||||||
// Used in script which only runs in environment with `@octokit/rest` installed
|
// Used in script which only runs in environment with `@octokit/rest` installed
|
||||||
"@octokit/rest",
|
"@octokit/rest",
|
||||||
// Used by `vitest`
|
|
||||||
"vitest-sonar-reporter",
|
|
||||||
// Used by `@babel/plugin-transform-runtime`
|
|
||||||
"@babel/runtime",
|
|
||||||
],
|
],
|
||||||
ignoreBinaries: [
|
ignoreBinaries: [
|
||||||
// Used when available by reusable workflow `.github/workflows/release-make.yml`
|
// Used when available by reusable workflow `.github/workflows/release-make.yml`
|
||||||
|
|||||||
+17
-14
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-js-sdk",
|
"name": "matrix-js-sdk",
|
||||||
"version": "41.1.0-rc.0",
|
"version": "41.3.0",
|
||||||
"description": "Matrix Client-Server SDK for Javascript",
|
"description": "Matrix Client-Server SDK for Javascript",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0"
|
"node": ">=22.0.0"
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
"lint:types": "tsc --noEmit",
|
"lint:types": "tsc --noEmit",
|
||||||
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
|
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
|
||||||
"lint:knip": "knip",
|
"lint:knip": "knip",
|
||||||
"test": "vitest",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest --watch",
|
"test:watch": "vitest watch",
|
||||||
"coverage": "pnpm test --coverage"
|
"coverage": "pnpm test --coverage"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm": "^17.1.0",
|
"@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
"bs58": "^6.0.0",
|
"bs58": "^6.0.0",
|
||||||
"content-type": "^1.0.4",
|
"content-type": "^1.0.4",
|
||||||
@@ -57,10 +57,9 @@
|
|||||||
"matrix-events-sdk": "0.0.1",
|
"matrix-events-sdk": "0.0.1",
|
||||||
"matrix-widget-api": "^1.16.1",
|
"matrix-widget-api": "^1.16.1",
|
||||||
"oidc-client-ts": "^3.0.1",
|
"oidc-client-ts": "^3.0.1",
|
||||||
"p-retry": "7",
|
"p-retry": "8",
|
||||||
"sdp-transform": "^3.0.0",
|
"sdp-transform": "^3.0.0",
|
||||||
"unhomoglyph": "^1.0.6",
|
"unhomoglyph": "^1.0.6"
|
||||||
"uuid": "13"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@action-validator/cli": "^0.6.0",
|
"@action-validator/cli": "^0.6.0",
|
||||||
@@ -106,21 +105,18 @@
|
|||||||
"fetch-mock": "^12.6.0",
|
"fetch-mock": "^12.6.0",
|
||||||
"happy-dom": "^20.1.0",
|
"happy-dom": "^20.1.0",
|
||||||
"husky": "^9.0.0",
|
"husky": "^9.0.0",
|
||||||
"knip": "^5.0.0",
|
"knip": "^6.0.0",
|
||||||
"lint-staged": "^16.0.0",
|
"lint-staged": "^16.0.0",
|
||||||
"matrix-mock-request": "^2.5.0",
|
"matrix-mock-request": "^2.5.0",
|
||||||
"prettier": "3.8.1",
|
"prettier": "3.8.3",
|
||||||
"typedoc": "^0.28.1",
|
"typedoc": "^0.28.1",
|
||||||
"typedoc-plugin-coverage": "^4.0.0",
|
"typedoc-plugin-coverage": "^4.0.0",
|
||||||
"typedoc-plugin-mdn-links": "^5.0.0",
|
"typedoc-plugin-mdn-links": "^5.0.0",
|
||||||
"typedoc-plugin-missing-exports": "^4.0.0",
|
"typedoc-plugin-missing-exports": "^4.0.0",
|
||||||
"typescript": "^5.4.2",
|
"typescript": "^6.0.0",
|
||||||
"vitest": "^4.0.17",
|
"vitest": "^4.0.17",
|
||||||
"vitest-sonar-reporter": "^3.0.0"
|
"vitest-sonar-reporter": "^3.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
|
||||||
"expect": "30.2.0"
|
|
||||||
},
|
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"peerDependencyRules": {
|
"peerDependencyRules": {
|
||||||
"allowedVersions": {
|
"allowedVersions": {
|
||||||
@@ -129,7 +125,14 @@
|
|||||||
},
|
},
|
||||||
"allowedDeprecatedVersions": {
|
"allowedDeprecatedVersions": {
|
||||||
"eslint": "8"
|
"eslint": "8"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"expect": "30.3.0",
|
||||||
|
"flatted@<=3.4.1": "^3.4.2",
|
||||||
|
"picomatch@>=4.0.0 <4.0.4": "^4.0.4",
|
||||||
|
"yaml@>=2.0.0 <2.8.3": "^2.8.3",
|
||||||
|
"vite": "8.0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
|
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1597
-1225
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,7 @@ import {
|
|||||||
encryptGroupSessionKey,
|
encryptGroupSessionKey,
|
||||||
encryptMegolmEvent,
|
encryptMegolmEvent,
|
||||||
encryptMegolmEventRawPlainText,
|
encryptMegolmEventRawPlainText,
|
||||||
|
encryptOlmEvent,
|
||||||
establishOlmSession,
|
establishOlmSession,
|
||||||
getTestOlmAccountKeys,
|
getTestOlmAccountKeys,
|
||||||
expectSendRoomKey,
|
expectSendRoomKey,
|
||||||
@@ -2064,6 +2065,7 @@ describe("crypto", () => {
|
|||||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||||
|
|
||||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||||
|
expect(verificationStatus.known).toBe(false); // We haven't actually stashed a copy of Alice's identity
|
||||||
expect(verificationStatus.isVerified()).toBe(false);
|
expect(verificationStatus.isVerified()).toBe(false);
|
||||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||||
@@ -2078,7 +2080,8 @@ describe("crypto", () => {
|
|||||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
|
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
|
||||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||||
|
|
||||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
|
||||||
|
expect(verificationStatus.known).toBe(true);
|
||||||
expect(verificationStatus.isVerified()).toBe(false);
|
expect(verificationStatus.isVerified()).toBe(false);
|
||||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||||
@@ -2089,7 +2092,8 @@ describe("crypto", () => {
|
|||||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
|
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
|
||||||
expect(hasCrossSigningKeysForUser).toBe(false);
|
expect(hasCrossSigningKeysForUser).toBe(false);
|
||||||
|
|
||||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus("@unknown:xyz");
|
||||||
|
expect(verificationStatus.known).toBe(false);
|
||||||
expect(verificationStatus.isVerified()).toBe(false);
|
expect(verificationStatus.isVerified()).toBe(false);
|
||||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||||
@@ -2119,6 +2123,7 @@ describe("crypto", () => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||||
|
expect(verificationStatus.known).toBe(true);
|
||||||
expect(verificationStatus.isVerified()).toBe(false);
|
expect(verificationStatus.isVerified()).toBe(false);
|
||||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||||
@@ -2311,4 +2316,107 @@ describe("crypto", () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("secret pushing", () => {
|
||||||
|
it("should push a new backup key when a new backup key is set", async () => {
|
||||||
|
// setup: alice has another device, DEVICE_ID, which is verified
|
||||||
|
const crypto = aliceClient.getCrypto()!;
|
||||||
|
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
|
||||||
|
await startClientAndAwaitFirstSync();
|
||||||
|
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
|
||||||
|
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
|
||||||
|
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
|
||||||
|
|
||||||
|
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
|
||||||
|
|
||||||
|
// when we set a new backup key
|
||||||
|
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||||
|
status: 404,
|
||||||
|
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||||
|
});
|
||||||
|
fetchMock.post("path:/_matrix/client/v3/room_keys/version", {
|
||||||
|
status: 200,
|
||||||
|
body: { version: "1" },
|
||||||
|
});
|
||||||
|
const secretPushPromise = new Promise<any>((resolve) => {
|
||||||
|
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
|
||||||
|
const content = JSON.parse(callLog.options.body as string);
|
||||||
|
resolve(content);
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await crypto.resetKeyBackup();
|
||||||
|
|
||||||
|
// we expect the other device to get a secret push
|
||||||
|
const content = await secretPushPromise;
|
||||||
|
const curve25519key = JSON.parse(testOlmAccount.identity_keys()).curve25519;
|
||||||
|
const ciphertext = content.messages["@alice:localhost"].DEVICE_ID.ciphertext[curve25519key];
|
||||||
|
const olmSession = new Olm.Session();
|
||||||
|
olmSession.create_inbound(testOlmAccount, ciphertext.body);
|
||||||
|
const decrypted = JSON.parse(olmSession.decrypt(0, ciphertext.body));
|
||||||
|
expect(decrypted.type).toBe("io.element.msc4385.secret.push");
|
||||||
|
expect(decrypted.content.name).toBe("m.megolm_backup.v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should receive pushed backup key", async () => {
|
||||||
|
// setup: alice has another device, DEVICE_ID, which is verified,
|
||||||
|
// and has a key backup set up and signed by DEVICE_ID
|
||||||
|
const crypto = aliceClient.getCrypto()!;
|
||||||
|
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
|
||||||
|
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||||
|
await startClientAndAwaitFirstSync();
|
||||||
|
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
|
||||||
|
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
|
||||||
|
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
|
||||||
|
|
||||||
|
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
|
||||||
|
|
||||||
|
// after we push the backup key to alice...
|
||||||
|
|
||||||
|
const senderIdentityKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||||
|
const aliceDeviceKeys = await crypto.getOwnDeviceKeys();
|
||||||
|
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
|
||||||
|
const secretPush = encryptOlmEvent({
|
||||||
|
sender: "@alice:localhost",
|
||||||
|
senderKey: senderIdentityKeys.curve25519,
|
||||||
|
senderSigningKey: senderIdentityKeys.ed25519,
|
||||||
|
p2pSession,
|
||||||
|
recipient: "@alice:localhost",
|
||||||
|
recipientCurve25519Key: aliceDeviceKeys.curve25519,
|
||||||
|
recipientEd25519Key: aliceDeviceKeys.ed25519,
|
||||||
|
plaincontent: {
|
||||||
|
secret: testData.BACKUP_DECRYPTION_KEY_BASE64,
|
||||||
|
name: "m.megolm_backup.v1",
|
||||||
|
},
|
||||||
|
plaintype: "io.element.msc4385.secret.push",
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncResponse = {
|
||||||
|
next_batch: 1,
|
||||||
|
to_device: {
|
||||||
|
events: [secretPush],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const backupKeyReceivedPromise = new Promise<string>((resolve) => {
|
||||||
|
aliceClient.on(CryptoEvent.KeyBackupDecryptionKeyCached, resolve);
|
||||||
|
});
|
||||||
|
const keyBackupEnabledPromise = new Promise<void>((resolve) => {
|
||||||
|
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||||
|
if (enabled) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||||
|
await syncPromise(aliceClient);
|
||||||
|
|
||||||
|
// alice should be using backup now
|
||||||
|
expect(await backupKeyReceivedPromise).toBe(testData.SIGNED_BACKUP_DATA.version);
|
||||||
|
await keyBackupEnabledPromise;
|
||||||
|
expect(await crypto.getActiveSessionBackupVersion()).toBe(testData.SIGNED_BACKUP_DATA.version);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
|
import { QrCodeData, QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||||
import fetchMock from "@fetch-mock/vitest";
|
import fetchMock from "@fetch-mock/vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -146,7 +146,7 @@ describe("MSC4108SignInWithQR", () => {
|
|||||||
|
|
||||||
const ourChannel = new MSC4108SecureChannel(ourMockSession);
|
const ourChannel = new MSC4108SecureChannel(ourMockSession);
|
||||||
const qrCodeData = QrCodeData.fromBytes(
|
const qrCodeData = QrCodeData.fromBytes(
|
||||||
await ourChannel.generateCode(QrCodeMode.Reciprocate, client.getDomain()!),
|
await ourChannel.generateCode(QrCodeIntent.Reciprocate, client.getDomain()!),
|
||||||
);
|
);
|
||||||
const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.publicKey);
|
const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.publicKey);
|
||||||
|
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
|
|||||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}`.
|
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}`.
|
||||||
* Contains the key from {prefix}MEGOLM_SESSION_DATA.
|
* Contains the key from {prefix}MEGOLM_SESSION_DATA.
|
||||||
*/
|
*/
|
||||||
@@ -422,7 +422,7 @@ def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tupl
|
|||||||
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
|
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
|
||||||
},
|
},
|
||||||
"forwarding_curve25519_key_chain": [],
|
"forwarding_curve25519_key_chain": [],
|
||||||
"org.matrix.msc3061.shared_history": True,
|
"m.shared_history": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
return megolm_export, private_key
|
return megolm_export, private_key
|
||||||
|
|||||||
@@ -3940,6 +3940,31 @@ describe("MatrixClient", function () {
|
|||||||
});
|
});
|
||||||
expect(httpLookups.length).toEqual(0);
|
expect(httpLookups.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should handle no jwks_uri", async () => {
|
||||||
|
const { jwks_uri: _, ...metadata } = mockOpenIdConfiguration();
|
||||||
|
httpLookups = [
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/auth_metadata`,
|
||||||
|
error: new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404),
|
||||||
|
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/auth_issuer`,
|
||||||
|
data: { issuer: metadata.issuer },
|
||||||
|
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
fetchMock.get("https://auth.org/.well-known/openid-configuration", metadata);
|
||||||
|
|
||||||
|
await expect(client.getAuthMetadata()).resolves.toEqual({
|
||||||
|
...metadata,
|
||||||
|
signingKeys: null,
|
||||||
|
});
|
||||||
|
expect(httpLookups.length).toEqual(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("identityHashedLookup", () => {
|
describe("identityHashedLookup", () => {
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ describe("CallMembership", () => {
|
|||||||
});
|
});
|
||||||
it("uses unpadded base64 for RTC backend identities", async () => {
|
it("uses unpadded base64 for RTC backend identities", async () => {
|
||||||
const membership = await CallMembership.parseFromEvent(makeMockEvent(0, { ...membershipTemplate }));
|
const membership = await CallMembership.parseFromEvent(makeMockEvent(0, { ...membershipTemplate }));
|
||||||
expect(membership.rtcBackendIdentity).toBe("j9N1u04ZbvI9qKf3cxrf2NauD-fIGJ4uAcYkfI9V7SY");
|
expect(membership.rtcBackendIdentity).toBe("jUZ0Q1yF5nV3LlAI5xfD1I7BPnAytJaPEAR57EXjJ6s");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 { computeRtcIdentityRaw } from "../../../src/matrixrtc/membershipData/index.ts";
|
||||||
|
|
||||||
|
describe("computeRtcIdentityRaw", () => {
|
||||||
|
it("should compute the correct identity hash", async () => {
|
||||||
|
// Test vector taken from the spec, with the expected output updated to match the unpadded base64 encoding
|
||||||
|
// https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md#appendix-hash-derivation-test-vectors
|
||||||
|
const result = await computeRtcIdentityRaw("@alice:example.com", "DEVICE123", "memberABC");
|
||||||
|
// Add assertions based on expected hash output
|
||||||
|
expect(result).toBe("J+T45tGruxc+HrUOqJJlyQSV33m728Cme4+vt8/SWrU");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -175,12 +175,29 @@ describe("oidc authorization", () => {
|
|||||||
|
|
||||||
expect(authUrl.searchParams.get("login_hint")).toEqual("login1234");
|
expect(authUrl.searchParams.get("login_hint")).toEqual("login1234");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should generate url with response_mode=fragment", async () => {
|
||||||
|
const nonce = "abc123";
|
||||||
|
|
||||||
|
const authUrl = new URL(
|
||||||
|
await generateOidcAuthorizationUrl({
|
||||||
|
metadata: delegatedAuthConfig,
|
||||||
|
homeserverUrl: baseUrl,
|
||||||
|
clientId,
|
||||||
|
redirectUri: baseUrl,
|
||||||
|
nonce,
|
||||||
|
responseMode: "fragment",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(authUrl.searchParams.get("response_mode")).toEqual("fragment");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("completeAuthorizationCodeGrant", () => {
|
describe("completeAuthorizationCodeGrant", () => {
|
||||||
const homeserverUrl = "https://server.org/";
|
const homeserverUrl = "https://server.org/";
|
||||||
const identityServerUrl = "https://id.org/";
|
const identityServerUrl = "https://id.org/";
|
||||||
const nonce = "test-nonce";
|
const nonce = "hRpB6pkE06";
|
||||||
const redirectUri = baseUrl;
|
const redirectUri = baseUrl;
|
||||||
const code = "auth_code_xyz";
|
const code = "auth_code_xyz";
|
||||||
const validBearerTokenResponse = {
|
const validBearerTokenResponse = {
|
||||||
@@ -290,6 +307,32 @@ describe("oidc authorization", () => {
|
|||||||
expect(queryParams.get("code")).toEqual(code);
|
expect(queryParams.get("code")).toEqual(code);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should make correct request to the token endpoint with response_mode=fragment", async () => {
|
||||||
|
const state = await setupState({ responseMode: "fragment" });
|
||||||
|
const codeVerifier = getValueFromStorage(state, "code_verifier");
|
||||||
|
await completeAuthorizationCodeGrant(code, state, "fragment");
|
||||||
|
|
||||||
|
expect(fetchMock.callHistory.lastCall(metadata.token_endpoint)?.options).toStrictEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "post",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
"accept": "application/json",
|
||||||
|
"content-type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// check body is correctly formed
|
||||||
|
const queryParams = fetchMock.callHistory.lastCall(metadata.token_endpoint)!.options
|
||||||
|
.body as URLSearchParams;
|
||||||
|
expect(queryParams.get("grant_type")).toEqual("authorization_code");
|
||||||
|
expect(queryParams.get("client_id")).toEqual(clientId);
|
||||||
|
expect(queryParams.get("code_verifier")).toEqual(codeVerifier);
|
||||||
|
expect(queryParams.get("redirect_uri")).toEqual(redirectUri);
|
||||||
|
expect(queryParams.get("code")).toEqual(code);
|
||||||
|
});
|
||||||
|
|
||||||
it("should return with valid bearer token", async () => {
|
it("should return with valid bearer token", async () => {
|
||||||
const state = await setupState();
|
const state = await setupState();
|
||||||
const scope = getValueFromStorage(state, "scope");
|
const scope = getValueFromStorage(state, "scope");
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type EstablishedEcies, QrCodeData, QrCodeMode, Ecies } from "@matrix-org/matrix-sdk-crypto-wasm";
|
import { type EstablishedEcies, QrCodeData, QrCodeIntent, Ecies } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||||
|
|
||||||
import { MSC4108RendezvousSession, MSC4108SecureChannel, PayloadType } from "../../../../src/rendezvous";
|
import { MSC4108RendezvousSession, MSC4108SecureChannel, PayloadType } from "../../../../src/rendezvous";
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ describe("MSC4108SecureChannel", () => {
|
|||||||
});
|
});
|
||||||
const channel = new MSC4108SecureChannel(session);
|
const channel = new MSC4108SecureChannel(session);
|
||||||
|
|
||||||
const code = await channel.generateCode(QrCodeMode.Login);
|
const code = await channel.generateCode(QrCodeIntent.Login);
|
||||||
expect(code).toHaveLength(71);
|
expect(code).toHaveLength(71);
|
||||||
const text = new TextDecoder().decode(code);
|
const text = new TextDecoder().decode(code);
|
||||||
expect(text.startsWith("MATRIX")).toBeTruthy();
|
expect(text.startsWith("MATRIX")).toBeTruthy();
|
||||||
@@ -43,7 +43,7 @@ describe("MSC4108SecureChannel", () => {
|
|||||||
} as unknown as MSC4108RendezvousSession;
|
} as unknown as MSC4108RendezvousSession;
|
||||||
const channel = new MSC4108SecureChannel(mockSession);
|
const channel = new MSC4108SecureChannel(mockSession);
|
||||||
|
|
||||||
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl));
|
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeIntent.Reciprocate, baseUrl));
|
||||||
const { initial_message: ciphertext } = new Ecies().establish_outbound_channel(
|
const { initial_message: ciphertext } = new Ecies().establish_outbound_channel(
|
||||||
qrCodeData.publicKey,
|
qrCodeData.publicKey,
|
||||||
"MATRIX_QR_CODE_LOGIN_INITIATE",
|
"MATRIX_QR_CODE_LOGIN_INITIATE",
|
||||||
@@ -64,7 +64,7 @@ describe("MSC4108SecureChannel", () => {
|
|||||||
vi.mocked(mockSession.receive).mockResolvedValue("");
|
vi.mocked(mockSession.receive).mockResolvedValue("");
|
||||||
await expect(channel.connect()).rejects.toThrow("No response from other device");
|
await expect(channel.connect()).rejects.toThrow("No response from other device");
|
||||||
|
|
||||||
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl));
|
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeIntent.Reciprocate, baseUrl));
|
||||||
const { initial_message: ciphertext } = new Ecies().establish_outbound_channel(
|
const { initial_message: ciphertext } = new Ecies().establish_outbound_channel(
|
||||||
qrCodeData.publicKey,
|
qrCodeData.publicKey,
|
||||||
"NOT_REAL_MATRIX_QR_CODE_LOGIN_INITIATE",
|
"NOT_REAL_MATRIX_QR_CODE_LOGIN_INITIATE",
|
||||||
@@ -87,7 +87,7 @@ describe("MSC4108SecureChannel", () => {
|
|||||||
} as unknown as MSC4108RendezvousSession;
|
} as unknown as MSC4108RendezvousSession;
|
||||||
channel = new MSC4108SecureChannel(mockSession);
|
channel = new MSC4108SecureChannel(mockSession);
|
||||||
|
|
||||||
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl));
|
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeIntent.Reciprocate, baseUrl));
|
||||||
const { channel: _opponentChannel, initial_message: ciphertext } = new Ecies().establish_outbound_channel(
|
const { channel: _opponentChannel, initial_message: ciphertext } = new Ecies().establish_outbound_channel(
|
||||||
qrCodeData.publicKey,
|
qrCodeData.publicKey,
|
||||||
"MATRIX_QR_CODE_LOGIN_INITIATE",
|
"MATRIX_QR_CODE_LOGIN_INITIATE",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
KeysQueryRequest,
|
KeysQueryRequest,
|
||||||
Migration,
|
Migration,
|
||||||
OlmMachine,
|
OlmMachine,
|
||||||
|
type OtherUserIdentity,
|
||||||
type PickledInboundGroupSession,
|
type PickledInboundGroupSession,
|
||||||
type PickledSession,
|
type PickledSession,
|
||||||
StoreHandle,
|
StoreHandle,
|
||||||
@@ -109,6 +110,7 @@ describe("initRustCrypto", () => {
|
|||||||
getBackupKeys: vi.fn(),
|
getBackupKeys: vi.fn(),
|
||||||
getIdentity: vi.fn().mockResolvedValue(null),
|
getIdentity: vi.fn().mockResolvedValue(null),
|
||||||
trackedUsers: vi.fn(),
|
trackedUsers: vi.fn(),
|
||||||
|
getAllRoomsPendingKeyBundles: vi.fn().mockResolvedValue([]),
|
||||||
} as unknown as Mocked<OlmMachine>;
|
} as unknown as Mocked<OlmMachine>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,6 +592,72 @@ describe("RustCrypto", () => {
|
|||||||
expect(res.length).toEqual(0);
|
expect(res.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(["m.room_key_bundle", "io.element.msc4268.room_key_bundle"])(
|
||||||
|
"should accept key bundles when we find out about them",
|
||||||
|
async (type: string) => {
|
||||||
|
// Given we are faking that the received to-device message is a
|
||||||
|
// decrypted room key bundle.
|
||||||
|
|
||||||
|
// @ts-ignore Overriding a private function
|
||||||
|
rustCrypto.receiveSyncChanges = vi.fn().mockReturnValue([keyBundleEvent(type)]);
|
||||||
|
|
||||||
|
// And that there is a pending key bundle
|
||||||
|
|
||||||
|
// @ts-ignore Overriding a private function
|
||||||
|
rustCrypto.olmMachine.getPendingKeyBundleDetailsForRoom = vi.fn().mockReturnValue({
|
||||||
|
inviteAcceptedAtMillis: Date.now(),
|
||||||
|
inviterId: { toString: vi.fn().mockReturnValue("@inv:s.co") },
|
||||||
|
});
|
||||||
|
|
||||||
|
// When we process to-device messages
|
||||||
|
rustCrypto.maybeAcceptKeyBundle = vi.fn().mockName("maybeAcceptKeyBundle").mockResolvedValue(null);
|
||||||
|
await rustCrypto.preprocessToDeviceMessages([]);
|
||||||
|
|
||||||
|
// Then we accepted the key bundle
|
||||||
|
expect(rustCrypto.maybeAcceptKeyBundle).toHaveBeenCalledWith("!r:s.co", "@inv:s.co");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should not accept other to-device messages as key bundles when we receive them", async () => {
|
||||||
|
// Given we are faking that the received to-device message looks
|
||||||
|
// like a room key bundle, except it has the wrong type.
|
||||||
|
|
||||||
|
// @ts-ignore Overriding a private function
|
||||||
|
rustCrypto.receiveSyncChanges = vi.fn().mockReturnValue([keyBundleEvent("foo.some_other_type")]);
|
||||||
|
|
||||||
|
// And that there is a pending key bundle
|
||||||
|
|
||||||
|
// @ts-ignore Overriding a private function
|
||||||
|
rustCrypto.olmMachine.getPendingKeyBundleDetailsForRoom = vi.fn().mockReturnValue({
|
||||||
|
inviteAcceptedAtMillis: Date.now(),
|
||||||
|
inviterId: { toString: vi.fn().mockReturnValue("@inv:s.co") },
|
||||||
|
});
|
||||||
|
|
||||||
|
// When we process to-device messages
|
||||||
|
rustCrypto.maybeAcceptKeyBundle = vi.fn().mockName("maybeAcceptKeyBundle").mockResolvedValue(null);
|
||||||
|
await rustCrypto.preprocessToDeviceMessages([]);
|
||||||
|
|
||||||
|
// Then we do not try to accepted a key bundle
|
||||||
|
expect(rustCrypto.maybeAcceptKeyBundle).not.toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
function keyBundleEvent(type: string): RustSdkCryptoJs.ProcessedToDeviceEvent {
|
||||||
|
return {
|
||||||
|
rawEvent: JSON.stringify({
|
||||||
|
content: { room_id: "!r:s.co" },
|
||||||
|
sender: "",
|
||||||
|
type,
|
||||||
|
}),
|
||||||
|
type: 0,
|
||||||
|
encryptionInfo: {
|
||||||
|
sender: "",
|
||||||
|
senderDevice: null,
|
||||||
|
senderCurve25519Key: "",
|
||||||
|
isSenderVerified: vi.fn().mockReturnValue(true),
|
||||||
|
},
|
||||||
|
} as any as RustSdkCryptoJs.ProcessedToDeviceEvent;
|
||||||
|
}
|
||||||
|
|
||||||
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
|
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
|
||||||
rustCrypto = await makeTestRustCrypto(
|
rustCrypto = await makeTestRustCrypto(
|
||||||
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
|
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
|
||||||
@@ -788,6 +856,7 @@ describe("RustCrypto", () => {
|
|||||||
undefined,
|
undefined,
|
||||||
secretStorage,
|
secretStorage,
|
||||||
);
|
);
|
||||||
|
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
|
||||||
|
|
||||||
async function createSecretStorageKey() {
|
async function createSecretStorageKey() {
|
||||||
return {
|
return {
|
||||||
@@ -835,6 +904,7 @@ describe("RustCrypto", () => {
|
|||||||
{} as CryptoCallbacks,
|
{} as CryptoCallbacks,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
|
||||||
|
|
||||||
async function createSecretStorageKey() {
|
async function createSecretStorageKey() {
|
||||||
return {
|
return {
|
||||||
@@ -1524,6 +1594,7 @@ describe("RustCrypto", () => {
|
|||||||
|
|
||||||
it("returns an unverified UserVerificationStatus when there is no UserIdentity", async () => {
|
it("returns an unverified UserVerificationStatus when there is no UserIdentity", async () => {
|
||||||
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
|
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
|
||||||
|
expect(userVerificationStatus.known).toBe(false);
|
||||||
expect(userVerificationStatus.isVerified()).toBeFalsy();
|
expect(userVerificationStatus.isVerified()).toBeFalsy();
|
||||||
expect(userVerificationStatus.isTofu()).toBeFalsy();
|
expect(userVerificationStatus.isTofu()).toBeFalsy();
|
||||||
expect(userVerificationStatus.isCrossSigningVerified()).toBeFalsy();
|
expect(userVerificationStatus.isCrossSigningVerified()).toBeFalsy();
|
||||||
@@ -1535,9 +1606,10 @@ describe("RustCrypto", () => {
|
|||||||
free: vi.fn(),
|
free: vi.fn(),
|
||||||
isVerified: vi.fn().mockReturnValue(true),
|
isVerified: vi.fn().mockReturnValue(true),
|
||||||
wasPreviouslyVerified: vi.fn().mockReturnValue(true),
|
wasPreviouslyVerified: vi.fn().mockReturnValue(true),
|
||||||
});
|
} as unknown as OtherUserIdentity);
|
||||||
|
|
||||||
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
|
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
|
||||||
|
expect(userVerificationStatus.known).toBe(true);
|
||||||
expect(userVerificationStatus.isVerified()).toBeTruthy();
|
expect(userVerificationStatus.isVerified()).toBeTruthy();
|
||||||
expect(userVerificationStatus.isTofu()).toBeFalsy();
|
expect(userVerificationStatus.isTofu()).toBeFalsy();
|
||||||
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
|
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
|
||||||
@@ -2320,6 +2392,7 @@ describe("RustCrypto", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
|
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
|
||||||
|
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
|
||||||
|
|
||||||
// We have a key backup
|
// We have a key backup
|
||||||
await waitFor(async () => expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull());
|
await waitFor(async () => expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull());
|
||||||
@@ -2388,6 +2461,7 @@ describe("RustCrypto", () => {
|
|||||||
queryKeysForUsers: vi.fn().mockReturnValue({}),
|
queryKeysForUsers: vi.fn().mockReturnValue({}),
|
||||||
getReceivedRoomKeyBundleData: vi.fn(),
|
getReceivedRoomKeyBundleData: vi.fn(),
|
||||||
receiveRoomKeyBundle: vi.fn(),
|
receiveRoomKeyBundle: vi.fn(),
|
||||||
|
clearRoomPendingKeyBundle: vi.fn(),
|
||||||
} as unknown as Mocked<OlmMachine>;
|
} as unknown as Mocked<OlmMachine>;
|
||||||
|
|
||||||
const http = new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
|
const http = new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
|
||||||
@@ -2448,6 +2522,10 @@ describe("RustCrypto", () => {
|
|||||||
expect(mockOlmMachine.receiveRoomKeyBundle).toHaveBeenCalledTimes(1);
|
expect(mockOlmMachine.receiveRoomKeyBundle).toHaveBeenCalledTimes(1);
|
||||||
expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][0]).toBe(bundleData);
|
expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][0]).toBe(bundleData);
|
||||||
expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][1]).toEqual(new TextEncoder().encode("asdfghjkl"));
|
expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][1]).toEqual(new TextEncoder().encode("asdfghjkl"));
|
||||||
|
|
||||||
|
// It should also flag the room as not waiting for a key bundle
|
||||||
|
expect(mockOlmMachine.clearRoomPendingKeyBundle).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockOlmMachine.clearRoomPendingKeyBundle.mock.calls[0][0].toString()).toEqual("!room_id");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||||
import { type OutgoingRequest } from "@matrix-org/matrix-sdk-crypto-wasm";
|
import { type OtherUserIdentity, type OutgoingRequest } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||||
import { type Mocked } from "vitest";
|
import { type Mocked } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -138,7 +138,9 @@ describe("VerificationRequest", () => {
|
|||||||
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
|
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
|
||||||
|
|
||||||
// Alice requests verification
|
// Alice requests verification
|
||||||
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
|
const bobUserIdentity = (await aliceOlmMachine.getIdentity(
|
||||||
|
new RustSdkCryptoJs.UserId(bobUserId),
|
||||||
|
)) as OtherUserIdentity;
|
||||||
|
|
||||||
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
|
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
|
||||||
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
|
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
|
||||||
@@ -276,7 +278,9 @@ describe("VerificationRequest", () => {
|
|||||||
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
|
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
|
||||||
|
|
||||||
// Alice requests verification
|
// Alice requests verification
|
||||||
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
|
const bobUserIdentity = (await aliceOlmMachine.getIdentity(
|
||||||
|
new RustSdkCryptoJs.UserId(bobUserId),
|
||||||
|
)) as OtherUserIdentity;
|
||||||
|
|
||||||
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
|
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
|
||||||
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
|
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
|
||||||
@@ -394,7 +398,9 @@ describe("VerificationRequest", () => {
|
|||||||
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
|
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
|
||||||
|
|
||||||
// Alice requests verification
|
// Alice requests verification
|
||||||
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
|
const bobUserIdentity = (await aliceOlmMachine.getIdentity(
|
||||||
|
new RustSdkCryptoJs.UserId(bobUserId),
|
||||||
|
)) as OtherUserIdentity;
|
||||||
|
|
||||||
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
|
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
|
||||||
const methods = [
|
const methods = [
|
||||||
|
|||||||
@@ -79,6 +79,41 @@ describe("IndexedDBStore", () => {
|
|||||||
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(2);
|
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should handle failed queries", async () => {
|
||||||
|
const store = new IndexedDBStore({
|
||||||
|
indexedDB: indexedDB,
|
||||||
|
dbName: "database",
|
||||||
|
localStorage,
|
||||||
|
});
|
||||||
|
await store.startup();
|
||||||
|
|
||||||
|
// Simulate a failed query
|
||||||
|
let txn: IDBRequest;
|
||||||
|
(store.backend as LocalIndexedDBStoreBackend)["db"]!.transaction = (): IDBTransaction => {
|
||||||
|
return {
|
||||||
|
objectStore: (name: string) =>
|
||||||
|
({
|
||||||
|
name,
|
||||||
|
openCursor: (query: unknown) => {
|
||||||
|
return (txn = {
|
||||||
|
error: new DOMException("Expected error"),
|
||||||
|
} as IDBRequest);
|
||||||
|
},
|
||||||
|
}) as IDBObjectStore,
|
||||||
|
} as IDBTransaction;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call backend directly as otherwise the error is masked.
|
||||||
|
const promise = store.backend.getClientOptions();
|
||||||
|
// The function uses a Promise.then(() => trick to delay execution
|
||||||
|
// so we need to wait before we can call the txn onerror handler.
|
||||||
|
process.nextTick(() => {
|
||||||
|
txn!.onerror!(new Event("we-ignore-this"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(() => promise).rejects.toThrow("selectQuery failed for client_options");
|
||||||
|
});
|
||||||
|
|
||||||
it("Should load presence events on startup", async () => {
|
it("Should load presence events on startup", async () => {
|
||||||
// 1. Create idb database
|
// 1. Create idb database
|
||||||
const indexedDB = new IDBFactory();
|
const indexedDB = new IDBFactory();
|
||||||
|
|||||||
+14
-2
@@ -416,9 +416,12 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents {
|
|||||||
[EventType.Direct]: { [userId: string]: string[] };
|
[EventType.Direct]: { [userId: string]: string[] };
|
||||||
[EventType.IgnoredUserList]: { ignored_users: { [userId: string]: EmptyObject } };
|
[EventType.IgnoredUserList]: { ignored_users: { [userId: string]: EmptyObject } };
|
||||||
"m.secret_storage.default_key": { key: string };
|
"m.secret_storage.default_key": { key: string };
|
||||||
// Flag set by the rust SDK (Element X) and also used by us to mark that the user opted out of backup
|
|
||||||
// (I don't know why it's m.org.matrix...)
|
// MSC4287: Sharing key backup preference between clients - used to mark that the user opted out of key storage
|
||||||
|
"m.key_backup": { enabled: boolean };
|
||||||
|
// MSC4287 unstable prefix (note the boolean property has the opposite sense)
|
||||||
"m.org.matrix.custom.backup_disabled": { disabled: boolean };
|
"m.org.matrix.custom.backup_disabled": { disabled: boolean };
|
||||||
|
|
||||||
"m.identity_server": { base_url: string | null };
|
"m.identity_server": { base_url: string | null };
|
||||||
[key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings;
|
[key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings;
|
||||||
[key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription;
|
[key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription;
|
||||||
@@ -428,6 +431,15 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents {
|
|||||||
[POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any };
|
[POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any };
|
||||||
|
|
||||||
[EventType.InvitePermissionConfig]: { default_action?: string };
|
[EventType.InvitePermissionConfig]: { default_action?: string };
|
||||||
|
|
||||||
|
// List of recently used reaction emojis
|
||||||
|
// https://spec.matrix.org/v1.18/client-server-api/#mrecent_emoji
|
||||||
|
"m.recent_emoji": {
|
||||||
|
recent_emoji: Array<{
|
||||||
|
emoji: string;
|
||||||
|
total: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ export interface InviteOpts {
|
|||||||
/**
|
/**
|
||||||
* Before sending the invite, if the room is encrypted, share the keys for any messages sent while the history
|
* Before sending the invite, if the room is encrypted, share the keys for any messages sent while the history
|
||||||
* visibility was `shared`, via the experimental
|
* visibility was `shared`, via the experimental
|
||||||
* support for [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268).
|
* support for [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268). If the room's current
|
||||||
|
* history visibility setting is neither `shared` nor `world_readable`, history sharing will be disabled to prevent
|
||||||
|
* exposing keys for messages sent prior to the visibility restriction.
|
||||||
*
|
*
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
|
|||||||
+12
-7
@@ -101,7 +101,7 @@ import {
|
|||||||
type RoomNameState,
|
type RoomNameState,
|
||||||
} from "./models/room.ts";
|
} from "./models/room.ts";
|
||||||
import { RoomMemberEvent, type RoomMemberEventHandlerMap } from "./models/room-member.ts";
|
import { RoomMemberEvent, type RoomMemberEventHandlerMap } from "./models/room-member.ts";
|
||||||
import { type IPowerLevelsContent, type RoomStateEvent, type RoomStateEventHandlerMap } from "./models/room-state.ts";
|
import { RoomStateEvent, type IPowerLevelsContent, type RoomStateEventHandlerMap } from "./models/room-state.ts";
|
||||||
import {
|
import {
|
||||||
isSendDelayedEventRequestOpts,
|
isSendDelayedEventRequestOpts,
|
||||||
UpdateDelayedEventAction,
|
UpdateDelayedEventAction,
|
||||||
@@ -2027,6 +2027,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
|
|
||||||
// attach the event listeners needed by RustCrypto
|
// attach the event listeners needed by RustCrypto
|
||||||
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
|
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
|
||||||
|
this.on(RoomStateEvent.Events, rustCrypto.onRoomStateEvent.bind(rustCrypto));
|
||||||
this.on(ClientEvent.Event, (event) => {
|
this.on(ClientEvent.Event, (event) => {
|
||||||
rustCrypto.onLiveEventFromSync(event);
|
rustCrypto.onLiveEventFromSync(event);
|
||||||
});
|
});
|
||||||
@@ -2428,12 +2429,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
|
|
||||||
const roomId = res.room_id;
|
const roomId = res.room_id;
|
||||||
if (opts.acceptSharedHistory && inviter && this.cryptoBackend) {
|
if (opts.acceptSharedHistory && inviter && this.cryptoBackend) {
|
||||||
|
// Flag upfront that we are waiting for a key bundle, so that if we crash mid-import, we can try again.
|
||||||
|
await this.cryptoBackend.markRoomAsPendingKeyBundle(roomId, inviter);
|
||||||
// Try to accept the room key bundle specified in a `m.room_key_bundle` to-device message we (might have) already received.
|
// Try to accept the room key bundle specified in a `m.room_key_bundle` to-device message we (might have) already received.
|
||||||
const bundleDownloaded = await this.cryptoBackend.maybeAcceptKeyBundle(roomId, inviter);
|
await this.cryptoBackend.maybeAcceptKeyBundle(roomId, inviter);
|
||||||
// If this fails, i.e. we haven't received this message yet, we need to wait until the to-device message arrives.
|
|
||||||
if (!bundleDownloaded) {
|
|
||||||
this.cryptoBackend.markRoomAsPendingKeyBundle(roomId, inviter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// In case we were originally given an alias, check the room cache again
|
// In case we were originally given an alias, check the room cache again
|
||||||
@@ -4088,7 +4087,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (opts.shareEncryptedHistory) {
|
if (opts.shareEncryptedHistory) {
|
||||||
await this.cryptoBackend?.shareRoomHistoryWithUser(roomId, userId);
|
const historyVisibility = this.getRoom(roomId)?.getHistoryVisibility() ?? HistoryVisibility.Shared;
|
||||||
|
// We should only share room history if the *current* visibility allows it.
|
||||||
|
if ([HistoryVisibility.Invited, HistoryVisibility.Joined].includes(historyVisibility)) {
|
||||||
|
this.logger.debug("Not sharing message history as the room history visibility is currently unshared");
|
||||||
|
} else {
|
||||||
|
await this.cryptoBackend?.shareRoomHistoryWithUser(roomId, userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.membershipChange(roomId, userId, KnownMembership.Invite, opts.reason);
|
return await this.membershipChange(roomId, userId, KnownMembership.Invite, opts.reason);
|
||||||
|
|||||||
@@ -80,6 +80,18 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
|||||||
*/
|
*/
|
||||||
importBackedUpRoomKeys(keys: IMegolmSessionData[], backupVersion: string, opts?: ImportRoomKeysOpts): Promise<void>;
|
importBackedUpRoomKeys(keys: IMegolmSessionData[], backupVersion: string, opts?: ImportRoomKeysOpts): Promise<void>;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Room key history sharing (MSC4268)
|
||||||
|
//
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share any shareable E2EE history in the given room with the given recipient,
|
||||||
|
* as per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
|
||||||
|
*/
|
||||||
|
shareRoomHistoryWithUser(roomId: string, userId: string): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Having accepted an invite for the given room from the given user, attempt to
|
* Having accepted an invite for the given room from the given user, attempt to
|
||||||
* find information about a room key bundle and, if found, download the
|
* find information about a room key bundle and, if found, download the
|
||||||
@@ -103,7 +115,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
|||||||
* @param roomId - The room we were invited to, for which we did not receive a key bundle before accepting the invite.
|
* @param roomId - The room we were invited to, for which we did not receive a key bundle before accepting the invite.
|
||||||
* @param inviterId - The user who invited us to the room and is expected to send the room key bundle.
|
* @param inviterId - The user who invited us to the room and is expected to send the room key bundle.
|
||||||
*/
|
*/
|
||||||
markRoomAsPendingKeyBundle(roomId: string, inviterId: string): void;
|
markRoomAsPendingKeyBundle(roomId: string, inviterId: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The methods which crypto implementations should expose to the Sync api
|
/** The methods which crypto implementations should expose to the Sync api
|
||||||
|
|||||||
+36
-19
@@ -188,7 +188,9 @@ export interface CryptoApi {
|
|||||||
/**
|
/**
|
||||||
* Check if the given user has published cross-signing keys.
|
* Check if the given user has published cross-signing keys.
|
||||||
*
|
*
|
||||||
* - If the user is tracked, a `/keys/query` request is made to update locally the cross signing keys.
|
* - If the user is this user, a `/keys/query` request is made to update locally the cross signing keys.
|
||||||
|
* - If the user is tracked, any current `/keys/query` requests are awaited (with a timeout) and then
|
||||||
|
* the locally cached information is used.
|
||||||
* - If the user is not tracked locally and downloadUncached is set to true,
|
* - If the user is not tracked locally and downloadUncached is set to true,
|
||||||
* a `/keys/query` request is made to the server to retrieve the cross signing keys.
|
* a `/keys/query` request is made to the server to retrieve the cross signing keys.
|
||||||
* - Otherwise, return false
|
* - Otherwise, return false
|
||||||
@@ -205,7 +207,10 @@ export interface CryptoApi {
|
|||||||
* Get the device information for the given list of users.
|
* Get the device information for the given list of users.
|
||||||
*
|
*
|
||||||
* For any users whose device lists are cached (due to sharing an encrypted room with the user), the
|
* For any users whose device lists are cached (due to sharing an encrypted room with the user), the
|
||||||
* cached device data is returned.
|
* cached device data is returned, unless it is stale.
|
||||||
|
*
|
||||||
|
* If there are users with stale cached entries, wait (with some timeout) for any in-progress
|
||||||
|
* `/keys/query` request to complete.
|
||||||
*
|
*
|
||||||
* If there are uncached users, and the `downloadUncached` parameter is set to `true`,
|
* If there are uncached users, and the `downloadUncached` parameter is set to `true`,
|
||||||
* a `/keys/query` request is made to the server to retrieve these devices.
|
* a `/keys/query` request is made to the server to retrieve these devices.
|
||||||
@@ -626,7 +631,6 @@ export interface CryptoApi {
|
|||||||
* * Disables 4S, deleting the info for the default key, the default key pointer itself and any
|
* * Disables 4S, deleting the info for the default key, the default key pointer itself and any
|
||||||
* known 4S data (cross-signing keys and the megolm key backup key).
|
* known 4S data (cross-signing keys and the megolm key backup key).
|
||||||
* * Deletes any dehydrated devices.
|
* * Deletes any dehydrated devices.
|
||||||
* * Sets the "m.org.matrix.custom.backup_disabled" account data flag to indicate that the user has disabled backups.
|
|
||||||
*/
|
*/
|
||||||
disableKeyStorage(): Promise<void>;
|
disableKeyStorage(): Promise<void>;
|
||||||
|
|
||||||
@@ -720,20 +724,6 @@ export interface CryptoApi {
|
|||||||
* @param secrets - The secrets bundle received from the other device
|
* @param secrets - The secrets bundle received from the other device
|
||||||
*/
|
*/
|
||||||
importSecretsBundle?(secrets: Awaited<ReturnType<SecretsBundle["to_json"]>>): Promise<void>;
|
importSecretsBundle?(secrets: Awaited<ReturnType<SecretsBundle["to_json"]>>): Promise<void>;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Room key history sharing (MSC4268)
|
|
||||||
//
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Share any shareable E2EE history in the given room with the given recipient,
|
|
||||||
* as per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
|
|
||||||
*
|
|
||||||
* @experimental
|
|
||||||
*/
|
|
||||||
shareRoomHistoryWithUser(roomId: string, userId: string): Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A reason code for a failure to decrypt an event. */
|
/** A reason code for a failure to decrypt an event. */
|
||||||
@@ -808,6 +798,9 @@ export enum DeviceIsolationModeKind {
|
|||||||
*
|
*
|
||||||
* Events from all senders are always decrypted (and should be decorated with message shields in case
|
* Events from all senders are always decrypted (and should be decorated with message shields in case
|
||||||
* of authenticity warnings, see {@link EventEncryptionInfo}).
|
* of authenticity warnings, see {@link EventEncryptionInfo}).
|
||||||
|
*
|
||||||
|
* `AllDevicesIsolationMode` is used in the legacy, non-'exclude insecure devices' mode in Element Web. It is not
|
||||||
|
* recommended (see {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4153 | MSC4153}).
|
||||||
*/
|
*/
|
||||||
export class AllDevicesIsolationMode {
|
export class AllDevicesIsolationMode {
|
||||||
public readonly kind = DeviceIsolationModeKind.AllDevicesIsolationMode;
|
public readonly kind = DeviceIsolationModeKind.AllDevicesIsolationMode;
|
||||||
@@ -834,6 +827,9 @@ export class AllDevicesIsolationMode {
|
|||||||
*
|
*
|
||||||
* Events are decrypted only if they come from a cross-signed device. Other events will result in a decryption
|
* Events are decrypted only if they come from a cross-signed device. Other events will result in a decryption
|
||||||
* failure. (To access the failure reason, see {@link MatrixEvent.decryptionFailureReason}.)
|
* failure. (To access the failure reason, see {@link MatrixEvent.decryptionFailureReason}.)
|
||||||
|
*
|
||||||
|
* `OnlySignedDevicesIsolationMode` corresponds to the 'Exclude insecure devices' mode in Element Web, which is
|
||||||
|
* recommended by {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4153 | MSC4153}.
|
||||||
*/
|
*/
|
||||||
export class OnlySignedDevicesIsolationMode {
|
export class OnlySignedDevicesIsolationMode {
|
||||||
public readonly kind = DeviceIsolationModeKind.OnlySignedDevicesIsolationMode;
|
public readonly kind = DeviceIsolationModeKind.OnlySignedDevicesIsolationMode;
|
||||||
@@ -865,6 +861,25 @@ export interface BootstrapCrossSigningOpts {
|
|||||||
* Represents the ways in which we trust a user
|
* Represents the ways in which we trust a user
|
||||||
*/
|
*/
|
||||||
export class UserVerificationStatus {
|
export class UserVerificationStatus {
|
||||||
|
/**
|
||||||
|
* Indicates if we have saved a known identity for this user. Typically, this means that we share a
|
||||||
|
* room with them (or have done in the past).
|
||||||
|
*
|
||||||
|
* If this is `false`, then the other flags ({@link isCrossSigningVerified}, {@link wasCrossSigningVerified},
|
||||||
|
* {@link needsUserApproval}) will also be `false`. This means that we haven't seen this user before.
|
||||||
|
*
|
||||||
|
* If this is `true`, then there are further possibilities:
|
||||||
|
*
|
||||||
|
* - If {@link isCrossSigningVerified} returns `true`, then we have cryptographically verified the current
|
||||||
|
* identity of this user: that is the highest form of trust we have.
|
||||||
|
*
|
||||||
|
* - If {@link needsUserApproval} is `true`, that means that the user has changed their identity.
|
||||||
|
*
|
||||||
|
* - Otherwise, the user is "TOFU trusted": we have a record of their identity, and, typically, will share
|
||||||
|
* encrypted content with them as long as they retain that identity.
|
||||||
|
*/
|
||||||
|
public readonly known: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if the identity has changed in a way that needs user approval.
|
* Indicates if the identity has changed in a way that needs user approval.
|
||||||
*
|
*
|
||||||
@@ -880,12 +895,14 @@ export class UserVerificationStatus {
|
|||||||
*/
|
*/
|
||||||
public readonly needsUserApproval: boolean;
|
public readonly needsUserApproval: boolean;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly crossSigningVerified: boolean,
|
private readonly crossSigningVerified: boolean,
|
||||||
private readonly crossSigningVerifiedBefore: boolean,
|
private readonly crossSigningVerifiedBefore: boolean,
|
||||||
private readonly tofu: boolean,
|
known: boolean,
|
||||||
needsUserApproval: boolean = false,
|
needsUserApproval: boolean = false,
|
||||||
) {
|
) {
|
||||||
|
this.known = known;
|
||||||
this.needsUserApproval = needsUserApproval;
|
this.needsUserApproval = needsUserApproval;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,7 +934,7 @@ export class UserVerificationStatus {
|
|||||||
* @deprecated No longer supported, with the Rust crypto stack.
|
* @deprecated No longer supported, with the Rust crypto stack.
|
||||||
*/
|
*/
|
||||||
public isTofu(): boolean {
|
public isTofu(): boolean {
|
||||||
return this.tofu;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -806,7 +806,7 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
// Sliding Sync
|
// Sliding Sync
|
||||||
await this.syncApi!.injectRoomEvents(this.room!, [event]);
|
await this.syncApi!.injectRoomEvents(this.room!, [event]);
|
||||||
}
|
}
|
||||||
logger.info(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`);
|
logger.debug(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`);
|
||||||
} else {
|
} else {
|
||||||
const { event_id: eventId, room_id: roomId } = ev.detail.data;
|
const { event_id: eventId, room_id: roomId } = ev.detail.data;
|
||||||
logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`);
|
logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`);
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export * from "./models/room-summary.ts";
|
|||||||
export * from "./models/event-status.ts";
|
export * from "./models/event-status.ts";
|
||||||
export * from "./models/profile-keys.ts";
|
export * from "./models/profile-keys.ts";
|
||||||
export * from "./models/related-relations.ts";
|
export * from "./models/related-relations.ts";
|
||||||
|
export { type StickyMatrixEvent, RoomStickyEventsEvent } from "./models/room-sticky-events.ts";
|
||||||
export type { RoomSummary } from "./client.ts";
|
export type { RoomSummary } from "./client.ts";
|
||||||
export * as ContentHelpers from "./content-helpers.ts";
|
export * as ContentHelpers from "./content-helpers.ts";
|
||||||
export * as SecretStorage from "./secret-storage.ts";
|
export * as SecretStorage from "./secret-storage.ts";
|
||||||
|
|||||||
@@ -863,7 +863,8 @@ function quickFilterNonRelevantContents(content: IContent, logger: Logger): bool
|
|||||||
// We have a MSC4143 event membership event with a proper joined content
|
// We have a MSC4143 event membership event with a proper joined content
|
||||||
return true;
|
return true;
|
||||||
} else if (eventKeysCount === 1 && "memberships" in content) {
|
} else if (eventKeysCount === 1 && "memberships" in content) {
|
||||||
logger.warn(`Legacy event found. Those are ignored, they do not contribute to the MatrixRTC session`);
|
// Events used to have this format in the past, but are now deprecated.
|
||||||
|
// Given that state events ~cannot be deleted, there can be some remaining events in the room, just ignore them.
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// Invalid or left content
|
// Invalid or left content
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import type { RelationType } from "../../types.ts";
|
|||||||
import { type RtcSlotEventContent, type Transport } from "../types.ts";
|
import { type RtcSlotEventContent, type Transport } from "../types.ts";
|
||||||
import { MatrixRTCMembershipParseError } from "./common.ts";
|
import { MatrixRTCMembershipParseError } from "./common.ts";
|
||||||
import { sha256 } from "../../digest.ts";
|
import { sha256 } from "../../digest.ts";
|
||||||
import { encodeUnpaddedBase64Url } from "../../base64.ts";
|
import { encodeUnpaddedBase64 } from "../../base64.ts";
|
||||||
import { slotIdToDescription } from "../utils.ts";
|
import { slotIdToDescription } from "../utils.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,8 +149,9 @@ export const checkRtcMembershipData = (data: IContent, sender: string): data is
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function computeRtcIdentityRaw(userId: string, deviceId: string, memberId: string): Promise<string> {
|
export async function computeRtcIdentityRaw(userId: string, deviceId: string, memberId: string): Promise<string> {
|
||||||
const hashInput = `${userId}|${deviceId}|${memberId}`;
|
// canonical JSON serialization (Matrix canonical JSON for arrays)
|
||||||
const hashBuffer = await sha256(hashInput);
|
const jsonStr = JSON.stringify([userId, deviceId, memberId]);
|
||||||
const hashedString = encodeUnpaddedBase64Url(hashBuffer);
|
const hashBuffer = await sha256(jsonStr);
|
||||||
|
const hashedString = encodeUnpaddedBase64(hashBuffer);
|
||||||
return hashedString;
|
return hashedString;
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-6
@@ -14,7 +14,15 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts";
|
import {
|
||||||
|
type IdTokenClaims,
|
||||||
|
Log,
|
||||||
|
OidcClient,
|
||||||
|
type SigninRequestCreateArgs,
|
||||||
|
SigninResponse,
|
||||||
|
SigninState,
|
||||||
|
WebStorageStateStore,
|
||||||
|
} from "oidc-client-ts";
|
||||||
|
|
||||||
import { logger } from "../logger.ts";
|
import { logger } from "../logger.ts";
|
||||||
import { secureRandomString } from "../randomstring.ts";
|
import { secureRandomString } from "../randomstring.ts";
|
||||||
@@ -127,6 +135,8 @@ export const generateAuthorizationUrl = async (
|
|||||||
* @param urlState - value to append to the opaque state identifier to uniquely identify the callback
|
* @param urlState - value to append to the opaque state identifier to uniquely identify the callback
|
||||||
* @param loginHint - value to send as the `login_hint` to the OP, giving a hint about the login identifier the user might use to log in.
|
* @param loginHint - value to send as the `login_hint` to the OP, giving a hint about the login identifier the user might use to log in.
|
||||||
* See {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest OIDC core 3.1.2.1}.
|
* See {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest OIDC core 3.1.2.1}.
|
||||||
|
* @param responseMode - value to send as the `response_mode` to the OP, selecting how auth is passed back during redirect.
|
||||||
|
* See {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest OIDC core 3.1.2.1}.
|
||||||
* @returns a Promise with the url as a string
|
* @returns a Promise with the url as a string
|
||||||
*/
|
*/
|
||||||
export const generateOidcAuthorizationUrl = async ({
|
export const generateOidcAuthorizationUrl = async ({
|
||||||
@@ -139,6 +149,7 @@ export const generateOidcAuthorizationUrl = async ({
|
|||||||
prompt,
|
prompt,
|
||||||
urlState,
|
urlState,
|
||||||
loginHint,
|
loginHint,
|
||||||
|
responseMode = "query",
|
||||||
}: {
|
}: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
metadata: ValidatedAuthMetadata;
|
metadata: ValidatedAuthMetadata;
|
||||||
@@ -149,6 +160,7 @@ export const generateOidcAuthorizationUrl = async ({
|
|||||||
prompt?: string;
|
prompt?: string;
|
||||||
urlState?: string;
|
urlState?: string;
|
||||||
loginHint?: string;
|
loginHint?: string;
|
||||||
|
responseMode?: SigninRequestCreateArgs["response_mode"];
|
||||||
}): Promise<string> => {
|
}): Promise<string> => {
|
||||||
const scope = generateScope();
|
const scope = generateScope();
|
||||||
const oidcClient = new OidcClient({
|
const oidcClient = new OidcClient({
|
||||||
@@ -156,7 +168,7 @@ export const generateOidcAuthorizationUrl = async ({
|
|||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
authority: metadata.issuer,
|
authority: metadata.issuer,
|
||||||
response_mode: "query",
|
response_mode: responseMode,
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
scope,
|
scope,
|
||||||
stateStore: new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }),
|
stateStore: new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }),
|
||||||
@@ -200,7 +212,8 @@ const normalizeBearerTokenResponseTokenType = (response: SigninResponse): Bearer
|
|||||||
* request to the Token Endpoint, to obtain the access token, refresh token, etc.
|
* request to the Token Endpoint, to obtain the access token, refresh token, etc.
|
||||||
*
|
*
|
||||||
* @param code - authorization code as returned by OP during authorization
|
* @param code - authorization code as returned by OP during authorization
|
||||||
* @param storedAuthorizationParams - stored params from start of oidc login flow
|
* @param state - authorization state param as returned by OP during authorization
|
||||||
|
* @param responseMode - the response mode used for authentication
|
||||||
* @returns valid bearer token response
|
* @returns valid bearer token response
|
||||||
* @throws An `Error` with `message` set to an entry in {@link OidcError},
|
* @throws An `Error` with `message` set to an entry in {@link OidcError},
|
||||||
* when the request fails, or the returned token response is invalid.
|
* when the request fails, or the returned token response is invalid.
|
||||||
@@ -208,6 +221,7 @@ const normalizeBearerTokenResponseTokenType = (response: SigninResponse): Bearer
|
|||||||
export const completeAuthorizationCodeGrant = async (
|
export const completeAuthorizationCodeGrant = async (
|
||||||
code: string,
|
code: string,
|
||||||
state: string,
|
state: string,
|
||||||
|
responseMode: SigninRequestCreateArgs["response_mode"] = "query",
|
||||||
): Promise<{
|
): Promise<{
|
||||||
oidcClientSettings: { clientId: string; issuer: string };
|
oidcClientSettings: { clientId: string; issuer: string };
|
||||||
tokenResponse: BearerTokenResponse;
|
tokenResponse: BearerTokenResponse;
|
||||||
@@ -221,13 +235,18 @@ export const completeAuthorizationCodeGrant = async (
|
|||||||
* so that oidc-client can parse it
|
* so that oidc-client can parse it
|
||||||
*/
|
*/
|
||||||
const reconstructedUrl = new URL(window.location.origin);
|
const reconstructedUrl = new URL(window.location.origin);
|
||||||
reconstructedUrl.searchParams.append("code", code);
|
|
||||||
reconstructedUrl.searchParams.append("state", state);
|
const params = new URLSearchParams({ code, state });
|
||||||
|
if (responseMode === "query") {
|
||||||
|
reconstructedUrl.search = params.toString();
|
||||||
|
} else {
|
||||||
|
reconstructedUrl.hash = `#${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
// set oidc-client to use our logger
|
// set oidc-client to use our logger
|
||||||
Log.setLogger(logger);
|
Log.setLogger(logger);
|
||||||
try {
|
try {
|
||||||
const response = new SigninResponse(reconstructedUrl.searchParams);
|
const response = new SigninResponse(params);
|
||||||
|
|
||||||
const stateStore = new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage });
|
const stateStore = new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage });
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,6 @@ export const validateAuthMetadataAndKeys = async (authMetadata: unknown): Promis
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...validatedIssuerConfig,
|
...validatedIssuerConfig,
|
||||||
signingKeys: await metadataService.getSigningKeys(),
|
signingKeys: validatedIssuerConfig.jwks_uri ? await metadataService.getSigningKeys() : null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
|
import { QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClientRendezvousFailureReason,
|
ClientRendezvousFailureReason,
|
||||||
@@ -108,7 +108,7 @@ interface SecretsPayload extends MSC4108Payload, Awaited<ReturnType<NonNullable<
|
|||||||
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
||||||
*/
|
*/
|
||||||
export class MSC4108SignInWithQR {
|
export class MSC4108SignInWithQR {
|
||||||
private readonly ourIntent: QrCodeMode;
|
private readonly ourIntent: QrCodeIntent;
|
||||||
private _code?: Uint8Array;
|
private _code?: Uint8Array;
|
||||||
private expectingNewDeviceId?: string;
|
private expectingNewDeviceId?: string;
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ export class MSC4108SignInWithQR {
|
|||||||
private readonly client?: MatrixClient,
|
private readonly client?: MatrixClient,
|
||||||
public onFailure?: RendezvousFailureListener,
|
public onFailure?: RendezvousFailureListener,
|
||||||
) {
|
) {
|
||||||
this.ourIntent = client ? QrCodeMode.Reciprocate : QrCodeMode.Login;
|
this.ourIntent = client ? QrCodeIntent.Reciprocate : QrCodeIntent.Login;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,9 +149,9 @@ export class MSC4108SignInWithQR {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.ourIntent === QrCodeMode.Reciprocate && this.client) {
|
if (this.ourIntent === QrCodeIntent.Reciprocate && this.client) {
|
||||||
this._code = await this.channel.generateCode(this.ourIntent, this.client.getDomain()!);
|
this._code = await this.channel.generateCode(this.ourIntent, this.client.getDomain()!);
|
||||||
} else if (this.ourIntent === QrCodeMode.Login) {
|
} else if (this.ourIntent === QrCodeIntent.Login) {
|
||||||
this._code = await this.channel.generateCode(this.ourIntent);
|
this._code = await this.channel.generateCode(this.ourIntent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ export class MSC4108SignInWithQR {
|
|||||||
* Returns true if the device is the already logged in device reciprocating a new login on the other side of the channel.
|
* Returns true if the device is the already logged in device reciprocating a new login on the other side of the channel.
|
||||||
*/
|
*/
|
||||||
public get isExistingDevice(): boolean {
|
public get isExistingDevice(): boolean {
|
||||||
return this.ourIntent === QrCodeMode.Reciprocate;
|
return this.ourIntent === QrCodeIntent.Reciprocate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
Ecies,
|
Ecies,
|
||||||
type EstablishedEcies,
|
type EstablishedEcies,
|
||||||
QrCodeData,
|
QrCodeData,
|
||||||
QrCodeMode,
|
QrCodeIntent,
|
||||||
} from "@matrix-org/matrix-sdk-crypto-wasm";
|
} from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -56,9 +56,9 @@ export class MSC4108SecureChannel {
|
|||||||
* @param mode the mode to generate the QR code in, either `Login` or `Reciprocate`.
|
* @param mode the mode to generate the QR code in, either `Login` or `Reciprocate`.
|
||||||
* @param serverName the name of the homeserver to connect to, as defined by server discovery in the spec, required for `Reciprocate` mode.
|
* @param serverName the name of the homeserver to connect to, as defined by server discovery in the spec, required for `Reciprocate` mode.
|
||||||
*/
|
*/
|
||||||
public async generateCode(mode: QrCodeMode.Login): Promise<Uint8Array>;
|
public async generateCode(mode: QrCodeIntent.Login): Promise<Uint8Array>;
|
||||||
public async generateCode(mode: QrCodeMode.Reciprocate, serverName: string): Promise<Uint8Array>;
|
public async generateCode(mode: QrCodeIntent.Reciprocate, serverName: string): Promise<Uint8Array>;
|
||||||
public async generateCode(mode: QrCodeMode, serverName?: string): Promise<Uint8Array> {
|
public async generateCode(mode: QrCodeIntent, serverName?: string): Promise<Uint8Array> {
|
||||||
const { url } = this.rendezvousSession;
|
const { url } = this.rendezvousSession;
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -68,7 +68,7 @@ export class MSC4108SecureChannel {
|
|||||||
return new QrCodeData(
|
return new QrCodeData(
|
||||||
this.secureChannel.public_key(),
|
this.secureChannel.public_key(),
|
||||||
url,
|
url,
|
||||||
mode === QrCodeMode.Reciprocate ? serverName : undefined,
|
mode === QrCodeIntent.Reciprocate ? serverName : undefined,
|
||||||
).toBytes();
|
).toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
-10
@@ -161,7 +161,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
|||||||
/**
|
/**
|
||||||
* Handles a backup secret received event and store it if it matches the current backup version.
|
* Handles a backup secret received event and store it if it matches the current backup version.
|
||||||
*
|
*
|
||||||
* @param secret - The secret as received from a `m.secret.send` event for secret `m.megolm_backup.v1`.
|
* @param secret - The secret as received from a `m.secret.send` or `io.element.msc4385.secret.push` event for secret `m.megolm_backup.v1`.
|
||||||
* @returns true if the secret is valid and has been stored, false otherwise.
|
* @returns true if the secret is valid and has been stored, false otherwise.
|
||||||
*/
|
*/
|
||||||
public async handleBackupSecretReceived(secret: string): Promise<boolean> {
|
public async handleBackupSecretReceived(secret: string): Promise<boolean> {
|
||||||
@@ -180,28 +180,44 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
|||||||
// There is no server-side key backup.
|
// There is no server-side key backup.
|
||||||
// This decryption key is useless to us.
|
// This decryption key is useless to us.
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
"handleBackupSecretReceived: Received a backup decryption key, but there is no trusted server-side key backup",
|
"handleBackupSecretReceived: Received a backup decryption key, but there is no server-side key backup",
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let backupDecryptionKey: RustSdkCryptoJs.BackupDecryptionKey;
|
||||||
|
try {
|
||||||
|
backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(secret);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn("handleBackupSecretReceived: Invalid backup decryption key", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(secret);
|
|
||||||
const privateKeyMatches = this.backupInfoMatchesBackupDecryptionKey(latestBackupInfo, backupDecryptionKey);
|
const privateKeyMatches = this.backupInfoMatchesBackupDecryptionKey(latestBackupInfo, backupDecryptionKey);
|
||||||
if (!privateKeyMatches) {
|
if (!privateKeyMatches) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`handleBackupSecretReceived: Private decryption key does not match the public key of the current remote backup.`,
|
`handleBackupSecretReceived: Private decryption key does not match the public key of the current server-side backup version (${latestBackupInfo.version})`,
|
||||||
);
|
);
|
||||||
// just ignore the secret
|
// just ignore the secret
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`handleBackupSecretReceived: A valid backup decryption key has been received and stored in cache.`,
|
`handleBackupSecretReceived: Valid decryption key for the current server-side backup version (${latestBackupInfo.version}) received`,
|
||||||
);
|
);
|
||||||
await this.saveBackupDecryptionKey(backupDecryptionKey, latestBackupInfo.version);
|
await this.saveBackupDecryptionKey(backupDecryptionKey, latestBackupInfo.version);
|
||||||
|
// Check if the backup should be enabled (e.g. if it's properly
|
||||||
|
// signed), and enable it if it should
|
||||||
|
if (this.keyBackupCheckInProgress) {
|
||||||
|
await this.keyBackupCheckInProgress;
|
||||||
|
}
|
||||||
|
this.keyBackupCheckInProgress = this.doCheckKeyBackup(latestBackupInfo).finally(() => {
|
||||||
|
this.keyBackupCheckInProgress = null;
|
||||||
|
});
|
||||||
|
await this.keyBackupCheckInProgress;
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn("handleBackupSecretReceived: Invalid backup decryption key", e);
|
this.logger.warn("handleBackupSecretReceived: Unable to validate backup decryption key", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -281,12 +297,17 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
|||||||
|
|
||||||
private keyBackupCheckInProgress: Promise<KeyBackupCheck | null> | null = null;
|
private keyBackupCheckInProgress: Promise<KeyBackupCheck | null> | null = null;
|
||||||
|
|
||||||
/** Helper for `checkKeyBackup` */
|
/** Helper to check the key backup status, and enable/disable it as appropriate
|
||||||
private async doCheckKeyBackup(): Promise<KeyBackupCheck | null> {
|
*
|
||||||
|
* A KeyBackupInfo can be passed if it was fetched recently, to avoid trying to
|
||||||
|
* re-fetch it from the server.
|
||||||
|
*/
|
||||||
|
private async doCheckKeyBackup(backupInfo?: KeyBackupInfo | null | undefined): Promise<KeyBackupCheck | null> {
|
||||||
this.logger.debug("Checking key backup status...");
|
this.logger.debug("Checking key backup status...");
|
||||||
let backupInfo: KeyBackupInfo | null | undefined;
|
|
||||||
try {
|
try {
|
||||||
backupInfo = await this.requestKeyBackupVersion();
|
if (!backupInfo) {
|
||||||
|
backupInfo = await this.requestKeyBackupVersion();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn("Error checking for active key backup", e);
|
this.logger.warn("Error checking for active key backup", e);
|
||||||
this.serverBackupInfo = undefined;
|
this.serverBackupInfo = undefined;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||||
import { StoreHandle } from "@matrix-org/matrix-sdk-crypto-wasm";
|
import { StoreHandle } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||||
|
|
||||||
import { RustCrypto } from "./rust-crypto.ts";
|
import { MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE, RustCrypto } from "./rust-crypto.ts";
|
||||||
import { type IHttpOpts, type MatrixHttpApi } from "../http-api/index.ts";
|
import { type IHttpOpts, type MatrixHttpApi } from "../http-api/index.ts";
|
||||||
import { type ServerSideSecretStorage } from "../secret-storage.ts";
|
import { type ServerSideSecretStorage } from "../secret-storage.ts";
|
||||||
import { type Logger } from "../logger.ts";
|
import { type Logger } from "../logger.ts";
|
||||||
@@ -247,5 +247,21 @@ async function initOlmMachine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have any recently-joined rooms, see if we have a pending key bundle for them.
|
||||||
|
for (const pendingDetails of await olmMachine.getAllRoomsPendingKeyBundles()) {
|
||||||
|
const roomId = pendingDetails.roomId.toString();
|
||||||
|
if (Date.now() - pendingDetails.inviteAcceptedAtMillis <= MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE) {
|
||||||
|
logger.info(
|
||||||
|
`Checking for pending key bundle for recently-joined room ${roomId} (joined ${new Date(pendingDetails.inviteAcceptedAtMillis).toISOString()})`,
|
||||||
|
);
|
||||||
|
await rustCrypto.maybeAcceptKeyBundle(roomId, pendingDetails.inviterId.toString());
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
`Clearing pending-key-bundle flag for room ${roomId} (too old: joined ${new Date(pendingDetails.inviteAcceptedAtMillis).toISOString()})`,
|
||||||
|
);
|
||||||
|
await olmMachine.clearRoomPendingKeyBundle(new RustSdkCryptoJs.RoomId(roomId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rustCrypto;
|
return rustCrypto;
|
||||||
}
|
}
|
||||||
|
|||||||
+133
-53
@@ -98,6 +98,7 @@ import { VerificationMethod } from "../types.ts";
|
|||||||
import { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
|
import { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
|
||||||
import { type UIAuthCallback } from "../interactive-auth.ts";
|
import { type UIAuthCallback } from "../interactive-auth.ts";
|
||||||
import { getHttpUriForMxc } from "../content-repo.ts";
|
import { getHttpUriForMxc } from "../content-repo.ts";
|
||||||
|
import { type RoomState } from "../matrix.ts";
|
||||||
|
|
||||||
const ALL_VERIFICATION_METHODS = [
|
const ALL_VERIFICATION_METHODS = [
|
||||||
VerificationMethod.Sas,
|
VerificationMethod.Sas,
|
||||||
@@ -111,6 +112,9 @@ interface ISignableObject {
|
|||||||
unsigned?: object;
|
unsigned?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The maximum time, in milliseconds, since we accepted an invite, that we should accept a key bundle. */
|
||||||
|
export const MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||||
*
|
*
|
||||||
@@ -131,9 +135,6 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
/** mapping of roomId → encryptor class */
|
/** mapping of roomId → encryptor class */
|
||||||
private roomEncryptors: Record<string, RoomEncryptor> = {};
|
private roomEncryptors: Record<string, RoomEncryptor> = {};
|
||||||
|
|
||||||
/** mapping of room ID -> inviter ID for rooms pending MSC4268 key bundles */
|
|
||||||
private readonly roomsPendingKeyBundles: Map<string, string> = new Map();
|
|
||||||
|
|
||||||
private eventDecryptor: EventDecryptor;
|
private eventDecryptor: EventDecryptor;
|
||||||
private keyClaimManager: KeyClaimManager;
|
private keyClaimManager: KeyClaimManager;
|
||||||
private outgoingRequestProcessor: OutgoingRequestProcessor;
|
private outgoingRequestProcessor: OutgoingRequestProcessor;
|
||||||
@@ -370,10 +371,10 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
/* allowRedirects */ true,
|
/* allowRedirects */ true,
|
||||||
/* useAuthentication */ true,
|
/* useAuthentication */ true,
|
||||||
);
|
);
|
||||||
let encryptedBundle: Blob;
|
let encryptedBundle: Uint8Array;
|
||||||
try {
|
try {
|
||||||
const bundleUrl = new URL(url);
|
const bundleUrl = new URL(url);
|
||||||
encryptedBundle = await this.http.authedRequest<Blob>(
|
const encryptedBundleBlob = await this.http.authedRequest<Blob>(
|
||||||
Method.Get,
|
Method.Get,
|
||||||
bundleUrl.pathname + bundleUrl.search,
|
bundleUrl.pathname + bundleUrl.search,
|
||||||
{},
|
{},
|
||||||
@@ -383,17 +384,24 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
prefix: "",
|
prefix: "",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
logger.info(`Received blob of length ${encryptedBundleBlob.size}`);
|
||||||
|
encryptedBundle = new Uint8Array(await encryptedBundleBlob.arrayBuffer());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`Error downloading encrypted bundle from ${url}:`, err);
|
logger.warn(`Error downloading encrypted bundle from ${url}:`, err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Received blob of length ${encryptedBundle.size}`);
|
|
||||||
try {
|
try {
|
||||||
await this.olmMachine.receiveRoomKeyBundle(bundleData, new Uint8Array(await encryptedBundle.arrayBuffer()));
|
await this.olmMachine.receiveRoomKeyBundle(bundleData, encryptedBundle);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`Error receiving encrypted bundle:`, err);
|
logger.warn(`Error receiving encrypted bundle:`, err);
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
|
} finally {
|
||||||
|
// Even if we were unable to import the bundle, we still clear the flag that indicates that we
|
||||||
|
// are waiting for the bundle to be received. The only reason this can happen is that the bundle was
|
||||||
|
// malformed somehow, so we don't want to keep retrying it.
|
||||||
|
await this.olmMachine.clearRoomPendingKeyBundle(new RustSdkCryptoJs.RoomId(roomId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -402,8 +410,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
/**
|
/**
|
||||||
* Implementation of {@link CryptoBackend.markRoomAsPendingKeyBundle}.
|
* Implementation of {@link CryptoBackend.markRoomAsPendingKeyBundle}.
|
||||||
*/
|
*/
|
||||||
public markRoomAsPendingKeyBundle(roomId: string, inviter: string): void {
|
public async markRoomAsPendingKeyBundle(roomId: string, inviter: string): Promise<void> {
|
||||||
this.roomsPendingKeyBundles.set(roomId, inviter);
|
await this.olmMachine.storeRoomPendingKeyBundle(
|
||||||
|
new RustSdkCryptoJs.RoomId(roomId),
|
||||||
|
new RustSdkCryptoJs.UserId(inviter),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -725,7 +736,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
? userIdentity.identityNeedsUserApproval()
|
? userIdentity.identityNeedsUserApproval()
|
||||||
: false;
|
: false;
|
||||||
userIdentity.free();
|
userIdentity.free();
|
||||||
return new UserVerificationStatus(verified, wasVerified, false, needsUserApproval);
|
return new UserVerificationStatus(verified, wasVerified, true, needsUserApproval);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -781,9 +792,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
* Implementation of {@link CryptoApi#getCrossSigningKeyId}
|
* Implementation of {@link CryptoApi#getCrossSigningKeyId}
|
||||||
*/
|
*/
|
||||||
public async getCrossSigningKeyId(type: CrossSigningKey = CrossSigningKey.Master): Promise<string | null> {
|
public async getCrossSigningKeyId(type: CrossSigningKey = CrossSigningKey.Master): Promise<string | null> {
|
||||||
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | undefined = await this.olmMachine.getIdentity(
|
const userIdentity = await this.getOwnIdentity();
|
||||||
new RustSdkCryptoJs.UserId(this.userId),
|
|
||||||
);
|
|
||||||
if (!userIdentity) {
|
if (!userIdentity) {
|
||||||
// The public keys are not available on this device
|
// The public keys are not available on this device
|
||||||
return null;
|
return null;
|
||||||
@@ -1012,9 +1021,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
* Implementation of {@link CryptoApi#getCrossSigningStatus}
|
* Implementation of {@link CryptoApi#getCrossSigningStatus}
|
||||||
*/
|
*/
|
||||||
public async getCrossSigningStatus(): Promise<CrossSigningStatus> {
|
public async getCrossSigningStatus(): Promise<CrossSigningStatus> {
|
||||||
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | null = await this.getOlmMachineOrThrow().getIdentity(
|
const userIdentity = await this.getOwnIdentity();
|
||||||
new RustSdkCryptoJs.UserId(this.userId),
|
|
||||||
);
|
|
||||||
|
|
||||||
const publicKeysOnDevice =
|
const publicKeysOnDevice =
|
||||||
Boolean(userIdentity?.masterKey) &&
|
Boolean(userIdentity?.masterKey) &&
|
||||||
@@ -1128,9 +1135,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
* Implementation of {@link CryptoApi#requestVerificationDM}
|
* Implementation of {@link CryptoApi#requestVerificationDM}
|
||||||
*/
|
*/
|
||||||
public async requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
|
public async requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
|
||||||
const userIdentity: RustSdkCryptoJs.OtherUserIdentity | undefined = await this.olmMachine.getIdentity(
|
const userIdentity = (await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(userId))) as
|
||||||
new RustSdkCryptoJs.UserId(userId),
|
| RustSdkCryptoJs.OtherUserIdentity
|
||||||
);
|
| undefined;
|
||||||
|
|
||||||
if (!userIdentity) throw new Error(`unknown userId ${userId}`);
|
if (!userIdentity) throw new Error(`unknown userId ${userId}`);
|
||||||
|
|
||||||
@@ -1214,9 +1221,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
* @returns a VerificationRequest when the request has been sent to the other party.
|
* @returns a VerificationRequest when the request has been sent to the other party.
|
||||||
*/
|
*/
|
||||||
public async requestOwnUserVerification(): Promise<VerificationRequest> {
|
public async requestOwnUserVerification(): Promise<VerificationRequest> {
|
||||||
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | undefined = await this.olmMachine.getIdentity(
|
const userIdentity = await this.getOwnIdentity();
|
||||||
new RustSdkCryptoJs.UserId(this.userId),
|
|
||||||
);
|
|
||||||
if (userIdentity === undefined) {
|
if (userIdentity === undefined) {
|
||||||
throw new Error("cannot request verification for this device when there is no existing cross-signing key");
|
throw new Error("cannot request verification for this device when there is no existing cross-signing key");
|
||||||
}
|
}
|
||||||
@@ -1371,6 +1376,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
public async resetKeyBackup(): Promise<void> {
|
public async resetKeyBackup(): Promise<void> {
|
||||||
const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o));
|
const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o));
|
||||||
|
|
||||||
|
await this.pushSecretToVerifiedDevices("m.megolm_backup.v1");
|
||||||
|
|
||||||
// we want to store the private key in 4S
|
// we want to store the private key in 4S
|
||||||
// need to check if 4S is set up?
|
// need to check if 4S is set up?
|
||||||
if (await this.secretStorageHasAESKey()) {
|
if (await this.secretStorageHasAESKey()) {
|
||||||
@@ -1726,34 +1733,37 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// If we have received a room key bundle message, and have previously marked the room
|
// If we have received a room key bundle message, and have recently joined the room in question,
|
||||||
// IDs it references as pending key bundles, tell the Rust SDK to try and accept it,
|
// tell the Rust SDK to try and accept the key bundle.
|
||||||
// just in case it was received after invite.
|
|
||||||
//
|
//
|
||||||
// We don't actually need to validate the contents of the bundle message, or do
|
// We don't actually need to validate the contents of the bundle message, or do
|
||||||
// anything with its contents at all. We simply want to inform the Rust SDK we have
|
// anything with its contents at all. We simply want to inform the Rust SDK we have
|
||||||
// received a new room key bundle that we might be able to download.
|
// received a new room key bundle that we might be able to download.
|
||||||
if (
|
if (isRoomKeyBundleMessage(parsedMessage)) {
|
||||||
isRoomKeyBundleMessage(parsedMessage) &&
|
const roomId = parsedMessage.content.room_id;
|
||||||
this.roomsPendingKeyBundles.has(parsedMessage.content.room_id)
|
const pendingDetails = await this.olmMachine.getPendingKeyBundleDetailsForRoom(
|
||||||
) {
|
new RustSdkCryptoJs.RoomId(roomId),
|
||||||
// No `await`-ing here, as this is called from inside the `/sync` loop.
|
|
||||||
this.maybeAcceptKeyBundle(
|
|
||||||
parsedMessage.content.room_id,
|
|
||||||
this.roomsPendingKeyBundles.get(parsedMessage.content.room_id)!,
|
|
||||||
).then(
|
|
||||||
(success) => {
|
|
||||||
if (success) {
|
|
||||||
this.roomsPendingKeyBundles.delete(parsedMessage.content.room_id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
this.logger.error(
|
|
||||||
`Error attempting to download key bundle for room ${parsedMessage.content.room_id}`,
|
|
||||||
);
|
|
||||||
this.logger.error(err);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
// Only accept the key bundle if we joined the room less than 24 hours ago.
|
||||||
|
if (!pendingDetails) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Not yet accepting key bundle for room where we are not awaiting a bundle: ${roomId}`,
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
Date.now() - pendingDetails.inviteAcceptedAtMillis >
|
||||||
|
MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE
|
||||||
|
) {
|
||||||
|
this.logger.info(
|
||||||
|
`Ignoring key bundle for room we joined too long ago: ${roomId}, joining time: ${new Date(pendingDetails.inviteAcceptedAtMillis).toISOString()}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.logger.info(`Considering key bundle for recently-joined room ${roomId}`);
|
||||||
|
// Don't block for the import to happen, here, as this is called from inside the `/sync` loop.
|
||||||
|
this.maybeAcceptKeyBundle(roomId, pendingDetails.inviterId.toString()).catch((err) => {
|
||||||
|
this.logger.error(`Error attempting to download key bundle for room ${roomId}`);
|
||||||
|
this.logger.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -1926,7 +1936,21 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
* @param oldMembership - The previous membership state. Null if it's a new member.
|
* @param oldMembership - The previous membership state. Null if it's a new member.
|
||||||
*/
|
*/
|
||||||
public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {
|
public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {
|
||||||
const enc = this.roomEncryptors[event.getRoomId()!];
|
const roomId = event.getRoomId()!;
|
||||||
|
|
||||||
|
// If it's our own membership, and we are no longer joined, clear any indication that we are waiting for a key
|
||||||
|
// bundle.
|
||||||
|
if (
|
||||||
|
oldMembership === KnownMembership.Join &&
|
||||||
|
member.membership !== KnownMembership.Join &&
|
||||||
|
member.userId === this.olmMachine.userId.toString()
|
||||||
|
) {
|
||||||
|
this.olmMachine.clearRoomPendingKeyBundle(new RustSdkCryptoJs.RoomId(roomId)).catch((e) => {
|
||||||
|
this.logger.error(`Error clearing room pending key bundle indicator for ${roomId}: ${e}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const enc = this.roomEncryptors[roomId];
|
||||||
if (!enc) {
|
if (!enc) {
|
||||||
// not encrypting in this room
|
// not encrypting in this room
|
||||||
return;
|
return;
|
||||||
@@ -1934,6 +1958,44 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
enc.onRoomMembership(member);
|
enc.onRoomMembership(member);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Previously, it was sufficient to check if we need to rotate the room key
|
||||||
|
* prior to sending a message. However, the history sharing feature
|
||||||
|
* (MSC4268) breaks this logic:
|
||||||
|
*
|
||||||
|
* 1. Alice sends a message M1 in room X;
|
||||||
|
* 2. Bob invites Charlie, who joins and immediately leaves the room;
|
||||||
|
* 3. Alice sends another message M2 in room X.
|
||||||
|
*
|
||||||
|
* Under the old logic, Alice would not rotate her key after Charlie
|
||||||
|
* leaves, resulting in M2 being encrypted with the same session as M1.
|
||||||
|
* This would allow Charlie to decrypt M2 if he ever gains access to
|
||||||
|
* the event.
|
||||||
|
*
|
||||||
|
* To counter this, we proactively discard any active outgoing Megolm
|
||||||
|
* session when we see an event indicating the user left.
|
||||||
|
*
|
||||||
|
* Note that we have to do this in `onRoomStateEvent` rather than
|
||||||
|
* `onRoomMembership`, because `onRoomMembership` is only called when we see
|
||||||
|
* a *change* in membership. In the case of a gappy sync, we might miss
|
||||||
|
* Charlie's invite and join, and only see the final `leave` event (so his
|
||||||
|
* membership goes from `leave` to `leave`).
|
||||||
|
*/
|
||||||
|
public onRoomStateEvent(event: MatrixEvent, _state: RoomState, _prevEvent: MatrixEvent | null): void {
|
||||||
|
if (event.getType() != EventType.RoomMember) {
|
||||||
|
// Ignore all events that aren't member updates.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.getStateKey()! !== this.olmMachine.userId.toString() &&
|
||||||
|
event.getContent().membership !== KnownMembership.Join
|
||||||
|
) {
|
||||||
|
this.logger.info(`Rotating session for room ${event.getRoomId()} due to member leaving the room`);
|
||||||
|
this.forceDiscardSession(event.getRoomId()!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Callback for OlmMachine.registerRoomKeyUpdatedCallback
|
/** Callback for OlmMachine.registerRoomKeyUpdatedCallback
|
||||||
*
|
*
|
||||||
* Called by the rust-sdk whenever there is an update to (megolm) room keys. We
|
* Called by the rust-sdk whenever there is an update to (megolm) room keys. We
|
||||||
@@ -2044,9 +2106,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
/**
|
/**
|
||||||
* Handles secret received from the rust secret inbox.
|
* Handles secret received from the rust secret inbox.
|
||||||
*
|
*
|
||||||
* The gossipped secrets are received using the `m.secret.send` event type
|
* The gossipped secrets are received using the `m.secret.send` or
|
||||||
* and are guaranteed to have been received over a 1-to-1 Olm
|
* `io.element.msc4385.secret.push` event types and are guaranteed to have
|
||||||
* Session from a verified device.
|
* been received over a 1-to-1 Olm Session from a verified device.
|
||||||
*
|
*
|
||||||
* The only secret currently handled in this way is `m.megolm_backup.v1`.
|
* The only secret currently handled in this way is `m.megolm_backup.v1`.
|
||||||
*
|
*
|
||||||
@@ -2189,7 +2251,22 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
* Used during migration from legacy js-crypto to update local trust if needed.
|
* Used during migration from legacy js-crypto to update local trust if needed.
|
||||||
*/
|
*/
|
||||||
public async getOwnIdentity(): Promise<RustSdkCryptoJs.OwnUserIdentity | undefined> {
|
public async getOwnIdentity(): Promise<RustSdkCryptoJs.OwnUserIdentity | undefined> {
|
||||||
return await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId));
|
const identity = (await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(this.userId))) as
|
||||||
|
| RustSdkCryptoJs.OwnUserIdentity
|
||||||
|
| undefined;
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a secret to all of the current user's verified devices.
|
||||||
|
*/
|
||||||
|
public async pushSecretToVerifiedDevices(name: string): Promise<void> {
|
||||||
|
const logger = new LogSpan(this.logger, "pushSecretToVerifiedDevices");
|
||||||
|
await this.keyClaimManager.ensureSessionsForUsers(logger, [new RustSdkCryptoJs.UserId(this.userId)]);
|
||||||
|
await this.olmMachine.pushSecretToVerifiedDevices(name);
|
||||||
|
this.outgoingRequestsManager.doProcessOutgoingRequests().catch((e) => {
|
||||||
|
logger.warn("pushSecretToVerifiedDevices: Error processing outgoing requests", e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2519,7 +2596,7 @@ function rustEncryptionInfoToJsEncryptionInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface RoomKeyBundleMessage {
|
interface RoomKeyBundleMessage {
|
||||||
type: "io.element.msc4268.room_key_bundle";
|
type: "m.room_key_bundle" | "io.element.msc4268.room_key_bundle";
|
||||||
content: {
|
content: {
|
||||||
room_id: string;
|
room_id: string;
|
||||||
};
|
};
|
||||||
@@ -2529,13 +2606,16 @@ interface RoomKeyBundleMessage {
|
|||||||
* Determines if the given payload is a RoomKeyBundleMessage.
|
* Determines if the given payload is a RoomKeyBundleMessage.
|
||||||
*
|
*
|
||||||
* A RoomKeyBundleMessage is identified by having a specific message type
|
* A RoomKeyBundleMessage is identified by having a specific message type
|
||||||
* ("io.element.msc4268.room_key_bundle") and a valid room_id in its content.
|
* ("m.room_key_bundle") and a valid room_id in its content.
|
||||||
*
|
*
|
||||||
* @param message - The received to-device message to check.
|
* @param message - The received to-device message to check.
|
||||||
* @returns True if the payload matches the RoomKeyBundleMessage structure, false otherwise.
|
* @returns True if the payload matches the RoomKeyBundleMessage structure, false otherwise.
|
||||||
*/
|
*/
|
||||||
function isRoomKeyBundleMessage(message: IToDeviceEvent): message is IToDeviceEvent & RoomKeyBundleMessage {
|
function isRoomKeyBundleMessage(message: IToDeviceEvent): message is IToDeviceEvent & RoomKeyBundleMessage {
|
||||||
return message.type === "io.element.msc4268.room_key_bundle" && typeof message.content.room_id === "string";
|
return (
|
||||||
|
(message.type === "io.element.msc4268.room_key_bundle" || message.type === "m.room_key_bundle") &&
|
||||||
|
typeof message.content.room_id === "string"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type CryptoEvents = (typeof CryptoEvent)[keyof typeof CryptoEvent];
|
type CryptoEvents = (typeof CryptoEvent)[keyof typeof CryptoEvent];
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const VERSION = DB_MIGRATIONS.length;
|
|||||||
* Return the data you want to keep.
|
* Return the data you want to keep.
|
||||||
* @returns Promise which resolves to an array of whatever you returned from
|
* @returns Promise which resolves to an array of whatever you returned from
|
||||||
* resultMapper.
|
* resultMapper.
|
||||||
|
* @throws If there was an error completing the query.
|
||||||
*/
|
*/
|
||||||
function selectQuery<T>(
|
function selectQuery<T>(
|
||||||
store: IDBObjectStore,
|
store: IDBObjectStore,
|
||||||
@@ -71,7 +72,7 @@ function selectQuery<T>(
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const results: T[] = [];
|
const results: T[] = [];
|
||||||
query.onerror = (): void => {
|
query.onerror = (): void => {
|
||||||
reject(new Error("Query failed: " + query.error?.name));
|
reject(new Error(`selectQuery failed for ${store.name}`, { cause: query.error }));
|
||||||
};
|
};
|
||||||
// collect results
|
// collect results
|
||||||
query.onsuccess = (): void => {
|
query.onsuccess = (): void => {
|
||||||
|
|||||||
+1
-2
@@ -21,7 +21,6 @@ limitations under the License.
|
|||||||
* This is an internal module. See {@link createNewMatrixCall} for the public API.
|
* This is an internal module. See {@link createNewMatrixCall} for the public API.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { parse as parseSdp, write as writeSdp } from "sdp-transform";
|
import { parse as parseSdp, write as writeSdp } from "sdp-transform";
|
||||||
|
|
||||||
import { logger } from "../logger.ts";
|
import { logger } from "../logger.ts";
|
||||||
@@ -2490,7 +2489,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
sender_session_id: this.client.getSessionId(),
|
sender_session_id: this.client.getSessionId(),
|
||||||
dest_session_id: this.opponentSessionId,
|
dest_session_id: this.opponentSessionId,
|
||||||
seq: toDeviceSeq,
|
seq: toDeviceSeq,
|
||||||
[ToDeviceMessageId]: uuidv4(),
|
[ToDeviceMessageId]: globalThis.crypto.randomUUID(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.emit(
|
this.emit(
|
||||||
|
|||||||
Reference in New Issue
Block a user