Compare commits

...

68 Commits

Author SHA1 Message Date
RiotRobot da3d5c4a43 v31.5.0 2024-03-12 18:33:38 +00:00
RiotRobot ee8413beff v31.5.0-rc.0 2024-03-05 14:03:49 +00:00
Michael Telatynski e4d4628cc8 When merging release notes, allow considering later versions in the same release cycle (#4085)
* When merging release notes, allow considering later versions in the same major.minor.patch set

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tweak comments

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-05 09:34:09 +00:00
David Langley b2e09250d9 Add job to automate adding new issues to the new project (#4087)
* add job to automate adding new issues to the new project

* missing jobs:
2024-02-29 12:20:53 +00:00
RiotRobot 6176faef48 Resetting package fields for development 2024-02-27 12:51:20 +00:00
RiotRobot 453cdd9eda Merge branch 'master' into develop 2024-02-27 12:51:18 +00:00
RiotRobot 6529f02c28 v31.4.0 2024-02-27 12:50:27 +00:00
Valere d3dfcd9242 Add basic retry for rust crypto outgoing requests (#4061)
* Add basic retry for outgoing requests

* Update doc

* Remove 504 from retryable

* Retry all 5xx and clarify client timeouts

* code review cleaning

* do not retry rust request if M_TOO_LARGE

* refactor use common retry alg between scheduler and rust requests

* Code review, cleaning and doc
2024-02-26 14:07:28 +00:00
Michael Telatynski a26fc46ed4 Update MSC2965 OIDC Discovery implementation (#4064) 2024-02-23 16:43:11 +00:00
renovate[bot] be3913e8a5 Update typedoc (#3958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 17:30:14 +00:00
renovate[bot] c1e0192baf Update dependency eslint-plugin-unicorn to v51 (#4083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 17:06:12 +00:00
Andy Balaam 8123e9a3f1 Bump matrix-react-sdk-crypto-wasm to v4.6.0 (#4082)
To reduce memory usage during export - see https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/pull/105
2024-02-22 11:55:27 +00:00
Michael Telatynski 0425f4e5c8 Merge remote-tracking branch 'origin/develop' into develop 2024-02-22 10:49:07 +00:00
Michael Telatynski 5b74b446d4 Prune broken docs symlinks
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-22 10:48:50 +00:00
renovate[bot] 624914a565 Update dependency husky to v9 (#4051)
* Update dependency husky to v9

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-22 09:46:30 +00:00
renovate[bot] 4da9627727 Update all non-major dependencies (#4076)
* Update all non-major dependencies

* Prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-22 09:35:20 +00:00
renovate[bot] 1cb30bfe9b Update typescript-eslint monorepo to v7 (#4078)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 09:04:17 +00:00
renovate[bot] 12308b4c07 Update dependency @types/node to v18.19.17 (#4075)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 09:02:45 +00:00
Michael Telatynski 7f25162725 Update pull_request.yaml 2024-02-21 21:31:43 +00:00
David Baker 4826868a8f Update stale comment (#4080)
And remove line that set it for it to just get overwritten
2024-02-21 19:01:16 +00:00
renovate[bot] 91bde6afa1 Update dependency typedoc-plugin-coverage to v3 (#4077)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-21 18:45:39 +00:00
RiotRobot 95842c2b91 v31.4.0-rc.0 2024-02-21 17:56:16 +00:00
Michael Telatynski c27c357688 Validate account_management_uri and account_management_actions_supported from OIDC Issuer well-known (#4074)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-21 14:56:11 +00:00
renovate[bot] b474439256 Update dependency oidc-client-ts to v3 (#4052)
* Update dependency oidc-client-ts to v3

* Update jwt-decode so that oidc-client-ts doesn't run its own and thus we can mock

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Merge

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Sort package.json

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Ensure oidc-client-ts 3.0.1 to drop crypto-js

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-19 17:20:56 +00:00
Michael Telatynski 42dc498359 Update pull_request.yaml 2024-02-19 13:49:48 +00:00
Michael Telatynski ca914c97e0 Allow specifying OIDC url state parameter for passing data to callback (#4068)
* Allow specifying more OIDC client metadata for dynamic registration

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Allow specifying url_state for dynamic oidc client registration

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Export NonEmptyArray type

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Allow specifying more OIDC client metadata for dynamic registration

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Export NonEmptyArray type

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-19 13:03:53 +00:00
Michael Telatynski f96dac1e5b Saner releases clean up (#4072)
* Remove allchange dependency

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove stale release scripts

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update pull request template to remove allchange behaviours

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update label check automation

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* mheap

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Re-add node-fetch which was previously transitive via allchange

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Use node-fetch@^2

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-19 12:32:26 +00:00
Michael Telatynski 7e0d92cbe0 Add getAuthIssuer method for MSC2965 (#4071)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-19 12:25:24 +00:00
Michael Telatynski fe46fec161 Allow specifying more OIDC client metadata for dynamic registration (#4070) 2024-02-16 14:43:52 +00:00
David Baker 2cf7d819d9 Add unread marker event type (#4069)
* Add unread marker event type

To support setting the 'marked unread' flag

* Await encrypted messages (#4063)

* await encrypted messages
+ fix comments

Signed-off-by: Timo K <toger5@hotmail.de>

* fix Tests

Signed-off-by: Timo K <toger5@hotmail.de>

* fix test

Signed-off-by: Timo K <toger5@hotmail.de>

* make sonar happy

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>

* Ignore memberships of users that are not in the call (#4065)

* ignore memberships of users that are not in the call

Signed-off-by: Timo K <toger5@hotmail.de>

* recompute memberships on room member change.

Signed-off-by: Timo K <toger5@hotmail.de>

* fix Tests and add test for left member

Signed-off-by: Timo K <toger5@hotmail.de>

* fix event type

Signed-off-by: Timo K <toger5@hotmail.de>

* fix import desaster

Signed-off-by: Timo K <toger5@hotmail.de>

* fix mocks

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Timo <16718859+toger5@users.noreply.github.com>
2024-02-16 10:34:15 +00:00
Timo 74c109adac Ignore memberships of users that are not in the call (#4065)
* ignore memberships of users that are not in the call

Signed-off-by: Timo K <toger5@hotmail.de>

* recompute memberships on room member change.

Signed-off-by: Timo K <toger5@hotmail.de>

* fix Tests and add test for left member

Signed-off-by: Timo K <toger5@hotmail.de>

* fix event type

Signed-off-by: Timo K <toger5@hotmail.de>

* fix import desaster

Signed-off-by: Timo K <toger5@hotmail.de>

* fix mocks

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2024-02-14 13:04:40 +00:00
Timo 5d7218476a Await encrypted messages (#4063)
* await encrypted messages
+ fix comments

Signed-off-by: Timo K <toger5@hotmail.de>

* fix Tests

Signed-off-by: Timo K <toger5@hotmail.de>

* fix test

Signed-off-by: Timo K <toger5@hotmail.de>

* make sonar happy

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2024-02-14 11:25:43 +00:00
RiotRobot d03db17405 Resetting package fields for development 2024-02-13 14:56:34 +00:00
RiotRobot 7520340c46 Merge branch 'master' into develop 2024-02-13 14:56:33 +00:00
RiotRobot b63845a413 v31.3.0 2024-02-13 14:55:35 +00:00
Andy Balaam 1b7695cdca Add AsJson forms of the key import/export methods (#4057) 2024-02-08 13:25:22 +00:00
Valere f4a796ca2f ElementR | Ensure own user and device trust are updated after migration before giving back control to the app. (#4059)
* Ensure own trust after olm migration

* Check legacy store contains data
2024-02-07 16:28:17 +00:00
Andy Balaam 58a5d09aed Bump matrix-sdk-crypto-wasm to 4.5.0 (#4060)
Fixes https://github.com/element-hq/element-web/issues/26948
2024-02-07 15:11:04 +00:00
RiotRobot bc620796c3 v31.3.0-rc.4 2024-02-06 15:36:22 +00:00
David Baker b1cfed1b21 Merge pull request #4056 from matrix-org/backport-4055-to-staging
[Backport staging] Add utility to check for non migrated legacy db
2024-02-06 14:09:14 +00:00
Valere c700d8daa2 Add utility to check for non migrated legacy db (#4055)
* Add utility to check for non migrated legacy db

* code review changes

* add unit tests for existsAndIsNotMigrated

* ensure indexeddb is clean for each state

(cherry picked from commit f94dbdec0f)
2024-02-06 13:18:48 +00:00
Valere f94dbdec0f Add utility to check for non migrated legacy db (#4055)
* Add utility to check for non migrated legacy db

* code review changes

* add unit tests for existsAndIsNotMigrated

* ensure indexeddb is clean for each state
2024-02-05 14:59:02 +00:00
renovate[bot] 173d9c331a Update dependency @types/jest to v29.5.12 (#4049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 17:39:51 +00:00
renovate[bot] 04ebcf7be7 Update peter-evans/repository-dispatch action to v3 (#4053)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 17:10:27 +00:00
renovate[bot] 5e185ae1e7 Update typescript-eslint monorepo to v6.20.0 (#4050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 17:08:12 +00:00
renovate[bot] 322cc6da10 Update babel monorepo to v7.23.9 (#4047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 16:56:15 +00:00
renovate[bot] 014e674a4e Update definitelyTyped (#4048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 16:55:59 +00:00
renovate[bot] 87acd9dd88 Update all non-major dependencies (#4046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 16:55:36 +00:00
Michael Telatynski bbccb98c06 Fix tag_name
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 16:26:40 +00:00
Michael Telatynski ca835a7cf7 Work around github actions id clash issue on release
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 16:22:15 +00:00
Michael Telatynski b8fb10a1d1 Fix merge-release-notes.js
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 16:09:44 +00:00
Michael Telatynski 5e9d2e064e Debug logging
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 16:07:20 +00:00
Michael Telatynski d2753a9aea Debug logging
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 16:03:52 +00:00
Michael Telatynski c6eda55110 Fix badly typed output
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 15:58:48 +00:00
Michael Telatynski 7ce243110f github-script is broken =(
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 15:55:14 +00:00
Michael Telatynski 20d26db37d Roll back to github-script v6 due to bugs
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 15:48:58 +00:00
Michael Telatynski a2a25e71ac Revert "Use ELEMENT_BOT_TOKEN for release-drafter-workflow.yml"
This reverts commit 1e7bc2f31c.
2024-02-02 15:44:43 +00:00
Michael Telatynski 1e7bc2f31c Use ELEMENT_BOT_TOKEN for release-drafter-workflow.yml
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 15:41:16 +00:00
Michael Telatynski eec5040bd0 Update release-drafter-workflow.yml 2024-02-02 15:36:57 +00:00
Michael Telatynski 24174c9233 Update release-drafter-workflow.yml 2024-02-02 15:33:38 +00:00
Michael Telatynski 8a2cd3f43c Update release-drafter-workflow.yml 2024-02-02 15:31:36 +00:00
Michael Telatynski eebf40590f Update release-drafter-workflow.yml 2024-02-02 15:21:54 +00:00
Michael Telatynski f5e0b3007b Update release-drafter-workflow.yml 2024-02-02 15:18:37 +00:00
RiotRobot 0d5b6138ae v31.3.0-rc.3 2024-02-02 15:00:42 +00:00
Michael Telatynski 45b02fed5a Update release-npm.yml 2024-02-02 14:59:30 +00:00
RiotRobot 4f63b47134 v31.3.0-rc.2 2024-02-02 14:53:13 +00:00
Michael Telatynski 6edf3990f6 Update release-make.yml 2024-02-02 14:51:49 +00:00
Michael Telatynski 7da620c5be Update release-drafter.yml 2024-02-02 14:40:28 +00:00
57 changed files with 1489 additions and 1467 deletions
-6
View File
@@ -5,9 +5,3 @@
- [ ] Tests written for new code (and old code if feasible)
- [ ] Linter and other CI checks pass
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
<!--
If you would like to specify text for the changelog entry other than your PR title, add the following:
Notes: Add super cool feature
-->
+1 -1
View File
@@ -16,7 +16,7 @@ concurrency:
jobs:
build-element-web:
name: Build element-web
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.90.0
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.92.0
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@bf47d102fdb849e755b0b0023ea3e81a44b6f570 # v2
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
+12 -4
View File
@@ -14,11 +14,18 @@ jobs:
name: Preview Changelog
runs-on: ubuntu-latest
steps:
- uses: matrix-org/allchange@main
- uses: mheap/github-action-required-labels@80a96a4863886addcbc9f681b5b295ba7f5424e1 # v5
if: github.event_name != 'merge_group'
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
requireLabel: true
labels: |
X-Breaking-Change
T-Deprecation
T-Enhancement
T-Defect
T-Task
Dependencies
mode: minimum
count: 1
prevent-blocked:
name: Prevent Blocked
@@ -39,6 +46,7 @@ jobs:
if: github.event.action == 'opened'
steps:
- name: Check membership
if: github.event.pull_request.user.login != 'renovate[bot]'
uses: tspascoal/get-user-teams-membership@ba78054988f58bea69b7c6136d563236f8ed2fc0 # v3
id: teams
with:
@@ -48,7 +56,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Add label
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@v7
with:
script: |
@@ -46,7 +46,8 @@ jobs:
if: inputs.include-changes
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ steps.release.outputs.id }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
DEPENDENCY: ${{ inputs.include-changes }}
VERSION: ${{ steps.draft-release.outputs.tag_name }}
with:
@@ -60,7 +61,7 @@ jobs:
if (DEPENDENCY.includes("/")) {
deps.push(DEPENDENCY.replace("$VERSION", VERSION))
} else {
const fromVersion = JSON.parse(await exec.exec("git show origin/master:package.json")).dependencies[DEPENDENCY];
const fromVersion = JSON.parse((await github.request(`https://raw.githubusercontent.com/${owner}/${repo}/master/package.json`)).data).dependencies[DEPENDENCY];
const toVersion = require("./package.json").dependencies[DEPENDENCY];
if (toVersion.endsWith("#develop")) {
@@ -82,5 +83,6 @@ jobs:
repo,
release_id: releaseId,
body: notes,
tag_name: VERSION,
});
}
+7 -7
View File
@@ -58,7 +58,7 @@ jobs:
fingerprint: ${{ inputs.gpg-fingerprint }}
- name: Get draft release
id: release
id: draft-release
uses: cardinalby/git-get-release-action@cedef2faf69cb7c55b285bad07688d04430b7ada # v1
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -91,7 +91,7 @@ jobs:
jq -e .scripts.dist package.json >/dev/null 2>&1 && HAS_DIST=1
echo "has-dist-script=$HAS_DIST" >> $GITHUB_OUTPUT
env:
VERSION: ${{ steps.release.outputs.tag_name }}
VERSION: ${{ steps.draft-release.outputs.tag_name }}
- name: Finalise version
if: inputs.final
@@ -159,7 +159,7 @@ jobs:
rm CHANGELOG.md.old
git add CHANGELOG.md
env:
RELEASE_NOTES: ${{ steps.release.outputs.body }}
RELEASE_NOTES: ${{ steps.draft-release.outputs.body }}
- name: Run pre-release script to update package.json fields
run: |
@@ -178,7 +178,7 @@ jobs:
uses: ./.action-repo/.github/actions/upload-release-assets
with:
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
upload-url: ${{ steps.release.outputs.upload_url }}
upload-url: ${{ steps.draft-release.outputs.upload_url }}
asset-path: ${{ inputs.asset-path }}
- name: Create signed tag
@@ -193,7 +193,7 @@ jobs:
uses: ./.action-repo/.github/actions/sign-release-tarball
with:
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
upload-url: ${{ steps.release.outputs.upload_url }}
upload-url: ${{ steps.draft-release.outputs.upload_url }}
# We defer pushing changes until after the release assets are built,
# signed & uploaded to improve the atomicity of this action.
@@ -214,7 +214,7 @@ jobs:
if: inputs.expected-asset-count
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ steps.release.outputs.id }}
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
with:
retries: 3
@@ -242,7 +242,7 @@ jobs:
- name: Publish release
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ steps.release.outputs.id }}
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
FINAL: ${{ inputs.final }}
with:
retries: 3
+4
View File
@@ -4,6 +4,10 @@ on:
secrets:
NPM_TOKEN:
required: true
outputs:
id:
description: "The npm package@version string we published"
value: ${{ jobs.npm.outputs.id }}
jobs:
npm:
name: Publish to npm
+1
View File
@@ -63,6 +63,7 @@ jobs:
yarn tpv purge --yes --out _docs --stale --major 10
yarn gendoc
symlinks -rc _docs
find _docs -xtype l -delete
- name: 🔨 Set up git
run: |
+14
View File
@@ -0,0 +1,14 @@
name: Move new issues into Issue triage board
on:
issues:
types: [opened]
jobs:
automate-project-columns-next:
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/element-hq/projects/120
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
+51
View File
@@ -1,3 +1,54 @@
Changes in [31.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.5.0) (2024-03-12)
==================================================================================================
## ✨ Features
* Update MSC2965 OIDC Discovery implementation ([#4064](https://github.com/matrix-org/matrix-js-sdk/pull/4064)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Add basic retry for rust crypto outgoing requests ([#4061](https://github.com/matrix-org/matrix-js-sdk/pull/4061)). Contributed by @BillCarsonFr.
Changes in [31.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.4.0) (2024-02-27)
==================================================================================================
## ✨ Features
* Validate `account_management_uri` and `account_management_actions_supported` from OIDC Issuer well-known ([#4074](https://github.com/matrix-org/matrix-js-sdk/pull/4074)). Contributed by @t3chguy.
* Allow specifying OIDC url state parameter for passing data to callback ([#4068](https://github.com/matrix-org/matrix-js-sdk/pull/4068)). Contributed by @t3chguy.
* Add getAuthIssuer method for MSC2965 ([#4071](https://github.com/matrix-org/matrix-js-sdk/pull/4071)). Contributed by @t3chguy.
* Allow specifying more OIDC client metadata for dynamic registration ([#4070](https://github.com/matrix-org/matrix-js-sdk/pull/4070)). Contributed by @t3chguy.
* Add unread marker event type ([#4069](https://github.com/matrix-org/matrix-js-sdk/pull/4069)). Contributed by @dbkr.
* Add "AsJson" forms of the key import/export methods ([#4057](https://github.com/matrix-org/matrix-js-sdk/pull/4057)). Contributed by @andybalaam.
## 🐛 Bug Fixes
* Ignore memberships of users that are not in the call ([#4065](https://github.com/matrix-org/matrix-js-sdk/pull/4065)). Contributed by @toger5.
* Await encrypted messages ([#4063](https://github.com/matrix-org/matrix-js-sdk/pull/4063)). Contributed by @toger5.
* ElementR | Ensure own user and device trust are updated after migration before giving back control to the app. ([#4059](https://github.com/matrix-org/matrix-js-sdk/pull/4059)). Contributed by @BillCarsonFr.
* Bump matrix-sdk-crypto-wasm to 4.5.0 ([#4060](https://github.com/matrix-org/matrix-js-sdk/pull/4060)). Contributed by @andybalaam.
Changes in [31.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.3.0) (2024-02-13)
==================================================================================================
## ✨ Features
* Add expire\_ts compatibility to matrixRTC ([#4032](https://github.com/matrix-org/matrix-js-sdk/pull/4032)). Contributed by @toger5.
* Element-R: support for migration of the room list from legacy crypto ([#4036](https://github.com/matrix-org/matrix-js-sdk/pull/4036)). Contributed by @richvdh.
* Element-R: check persistent room list for encryption config ([#4035](https://github.com/matrix-org/matrix-js-sdk/pull/4035)). Contributed by @richvdh.
* Support optional MSC3860 redirects ([#4007](https://github.com/matrix-org/matrix-js-sdk/pull/4007)). Contributed by @turt2live.
## 🐛 Bug Fixes
* WebR: migrate the megolm session imported flag ([#4037](https://github.com/matrix-org/matrix-js-sdk/pull/4037)). Contributed by @BillCarsonFr.
* ElementR: fix emoji verification stalling when both ends hit start at the same time ([#4004](https://github.com/matrix-org/matrix-js-sdk/pull/4004)). Contributed by @uhoreg.
* Dependencies: Bump wasm bindings version to 4.3.0 ([#4042](https://github.com/matrix-org/matrix-js-sdk/pull/4042)). Contributed by @BillCarsonFr.
* Element R: emit events when devices have changed ([#4019](https://github.com/matrix-org/matrix-js-sdk/pull/4019)). Contributed by @uhoreg.
* ElementR: report invalid keys rather than failing to restore from backup ([#4006](https://github.com/matrix-org/matrix-js-sdk/pull/4006)). Contributed by @uhoreg.
* Make `timeline` a getter ([#4022](https://github.com/matrix-org/matrix-js-sdk/pull/4022)). Contributed by @florianduros.
* Implement getting verification cancellation info in Rust crypto ([#3947](https://github.com/matrix-org/matrix-js-sdk/pull/3947)). Contributed by @uhoreg.
* Fix crypto migration for megolm sessions with no sender key ([#4024](https://github.com/matrix-org/matrix-js-sdk/pull/4024)). Contributed by @richvdh.
Changes in [31.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.2.0) (2024-01-31)
==================================================================================================
## ✨ Features
+12 -11
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "31.3.0-rc.1",
"version": "31.5.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=18.0.0"
@@ -52,15 +52,15 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^4.3.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^4.6.0",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"jwt-decode": "^3.1.2",
"jwt-decode": "^4.0.0",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.6.0",
"oidc-client-ts": "^2.2.4",
"oidc-client-ts": "^3.0.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
@@ -83,6 +83,7 @@
"@babel/register": "^7.12.10",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@matrix-org/olm": "3.2.15",
"@peculiar/webcrypto": "^1.4.5",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7",
@@ -91,9 +92,8 @@
"@types/node": "18",
"@types/sdp-transform": "^2.4.5",
"@types/uuid": "9",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"allchange": "^1.3.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"babel-jest": "^29.0.0",
"debug": "^4.3.4",
"domexception": "^4.0.0",
@@ -106,22 +106,23 @@
"eslint-plugin-jsdoc": "^48.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^50.0.0",
"eslint-plugin-unicorn": "^51.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "9.11.0",
"fetch-mock-jest": "^1.5.1",
"husky": "^8.0.3",
"husky": "^9.0.0",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"lint-staged": "^15.0.2",
"matrix-mock-request": "^2.5.0",
"prettier": "3.2.4",
"node-fetch": "^2.7.0",
"prettier": "3.2.5",
"rimraf": "^5.0.0",
"ts-node": "^10.9.1",
"typedoc": "^0.24.0",
"typedoc-plugin-coverage": "^2.1.0",
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-missing-exports": "^2.0.0",
"typedoc-plugin-versions": "^0.2.3",
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
#
# Script to perform a post-release steps of matrix-js-sdk.
#
# Requires:
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
"$(dirname "$0")/scripts/release/post-merge-master.sh"
git push origin develop
fi
-346
View File
@@ -1,346 +0,0 @@
#!/bin/bash
#
# Script to perform a release of matrix-js-sdk and downstream projects.
#
# Requires:
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
#
# Note: this script is also used to release matrix-react-sdk, element-web, and element-desktop.
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [[ $(command -v hub) ]] && [[ $(hub --version) =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
echo "hub version 2.5 is required, you have $HUB_VERSION_MAJOR.$HUB_VERSION_MINOR installed"
exit
fi
else
echo "hub is required: please install it"
exit
fi
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
help() {
cat <<EOF
$USAGE
-c changelog_file: specify name of file containing changelog
-x: skip updating the changelog
EOF
}
if ! git diff-index --quiet --cached HEAD; then
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
exit
fi
if ! git diff-files --quiet; then
echo "this git checkout has uncommitted changes. Refusing to release."
exit
fi
skip_changelog=
changelog_file="CHANGELOG.md"
while getopts hc:x f; do
case $f in
h)
help
exit 0
;;
c)
changelog_file="$OPTARG"
;;
x)
skip_changelog=1
;;
esac
done
shift $(expr $OPTIND - 1)
if [ $# -ne 1 ]; then
echo "Usage: $USAGE" >&2
exit 1
fi
function check_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Checking version of $1..."
local latestver=$(yarn info -s "$1" dist-tags.next)
if [ "$depver" != "$latestver" ]
then
echo "The latest version of $1 is $latestver but package.json depends on $depver."
echo -n "Type 'u' to auto-upgrade, 'c' to continue anyway, or 'a' to abort:"
read resp
if [ "$resp" != "u" ] && [ "$resp" != "c" ]
then
echo "Aborting."
exit 1
fi
if [ "$resp" == "u" ]
then
echo "Upgrading $1 to $latestver..."
yarn add -E "$1@$latestver"
git add -u
git commit -m "Upgrade $1 to $latestver"
fi
fi
}
function reset_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Resetting $1 to develop branch..."
yarn add "github:matrix-org/$1#develop"
git add -u
git commit -m "Reset $1 back to develop branch"
}
has_subprojects=0
if [ -f release_config.yaml ]; then
subprojects=$(cat release_config.yaml | python -c "import yaml; import sys; print(' '.join(list(yaml.load(sys.stdin)['subprojects'].keys())))" 2> /dev/null)
if [ "$?" -eq 0 ]; then
has_subprojects=1
echo "Checking subprojects for upgrades"
for proj in $subprojects; do
check_dependency "$proj"
done
fi
fi
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
# global cache here to ensure we get the right thing.
yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts --frozen-lockfile
# ignore leading v on release
release="${1#v}"
tag="v${release}"
prerelease=0
# We check if this build is a prerelease by looking to
# see if the version has a hyphen in it. Crude,
# but semver doesn't support postreleases so anything
# with a hyphen is a prerelease.
echo $release | grep -q '-' && prerelease=1
if [ $prerelease -eq 1 ]; then
echo Making a PRE-RELEASE
else
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
fi
rel_branch=$(git symbolic-ref --short HEAD)
if [ -z "$skip_changelog" ]; then
echo "Generating changelog"
yarn run allchange "$release"
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
echo "Committing updated changelog"
git commit "$changelog_file" -m "Prepare changelog for $tag"
fi
fi
latest_changes=$(mktemp)
cat "${changelog_file}" | "$(dirname "$0")/scripts/changelog_head.py" > "${latest_changes}"
set -x
# Bump package.json and build the dist
echo "yarn version"
# yarn version will automatically commit its modification
# and make a release tag. We don't want it to create the tag
# because it can only sign with the default key, but we can
# only turn off both of these behaviours, so we have to
# manually commit the result.
yarn version --no-git-tag-version --new-version "$release"
"$(dirname "$0")/scripts/release/pre-release.sh"
# commit yarn.lock if it exists, is versioned, and is modified
if [[ -f yarn.lock && $(git status --porcelain yarn.lock | grep '^ M') ]];
then
pkglock='yarn.lock'
else
pkglock=''
fi
git commit package.json $pkglock -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
result=$(cat release_config.yaml | python -c "import yaml; import sys; print(yaml.load(sys.stdin)['signing_id'])" 2> /dev/null || true)
if [ "$?" -eq 0 ]; then
signing_id=$result
fi
fi
# If there is a 'dist' script in the package.json,
# run it in a separate checkout of the project, then
# upload any files in the 'dist' directory as release
# assets.
# We make a completely separate checkout to be sure
# we're using released versions of the dependencies
# (rather than whatever we're pulling in from yarn link)
assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
if [ $dodist -eq 0 ]; then
projdir=$(pwd)
builddir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
echo "Building distribution copy in $builddir"
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
yarn install --frozen-lockfile
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" yarn dist
popd
for i in "$builddir"/dist/*; do
assets="$assets -a $i"
if [ -n "$signing_id" ]
then
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
assets="$assets -a $i.asc"
fi
done
fi
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=$(tty) git tag -u "$signing_id" -F "${latest_changes}" "$tag"
else
git tag -a -F "${latest_changes}" "$tag"
fi
# push the tag and the release branch
git push origin "$rel_branch" "$tag"
if [ -n "$signing_id" ]; then
# make a signature for the source tarball.
#
# github will make us a tarball from the tag - we want to create a
# signature for it, which means that first of all we need to check that
# it's correct.
#
# we can't deterministically build exactly the same tarball, due to
# differences in gzip implementation - but we *can* build the same tar - so
# the easiest way to check the validity of the tarball from git is to unzip
# it and compare it with our own idea of what the tar should look like.
# This uses git archive which seems to be what github uses. Specifically,
# the header fields are set in the same way: same file mode, uid & gid
# both zero and mtime set to the timestamp of the commit that the tag
# references. Also note that this puts the commit into the tar headers
# and can be extracted with gunzip -c foo.tar.gz | git get-tar-commit-id
# the name of the sig file we want to create
source_sigfile="${tag}-src.tar.gz.asc"
tarfile="$tag.tar.gz"
gh_project_url=$(git remote get-url origin |
sed -e 's#^git@github\.com:#https://github.com/#' \
-e 's#^git\+ssh://git@github\.com/#https://github.com/#' \
-e 's/\.git$//')
project_name="${gh_project_url##*/}"
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
# unzip it and compare it with the tar we would generate
if ! cmp --silent <(gunzip -c $tarfile) \
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
# we don't bail out here, because really it's more likely that our comparison
# screwed up and it's super annoying to abort the script at this point.
cat >&2 <<EOF
!!!!!!!!!!!!!!!!!
!!!! WARNING !!!!
Mismatch between our own tarfile and that generated by github: not signing
source tarball.
To resolve, determine if $tarfile is correct, and if so sign it with gpg and
attach it to the release as $source_sigfile.
!!!!!!!!!!!!!!!!!
EOF
else
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig "$tarfile"
assets="$assets -a $source_sigfile"
fi
fi
hubflags=''
if [ $prerelease -eq 1 ]; then
hubflags='-p'
fi
release_text=$(mktemp)
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -F "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
fi
rm "${release_text}"
rm "${latest_changes}"
# if it is a pre-release, leave it on the release branch for now.
if [ $prerelease -eq 1 ]; then
git checkout "$rel_branch"
exit 0
fi
# merge release branch to master
echo "updating master branch"
git checkout master
git pull
git merge "$rel_branch" --no-edit
# push master to github
git push origin master
# finally, merge master back onto develop (if it exists)
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
git checkout develop
git pull
git merge master --no-edit
git push origin develop
fi
[ -x ./post-release.sh ] && ./post-release.sh
if [ $has_subprojects -eq 1 ] && [ $prerelease -eq 0 ]; then
echo "Resetting subprojects to develop"
for proj in $subprojects; do
reset_dependency "$proj"
done
git push origin develop
fi
+42 -13
View File
@@ -2,21 +2,29 @@
const fs = require("fs");
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
// Or it can be a string in the form owner/repo@tag
// Or it can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
async function listReleases(github, owner, repo) {
const response = await github.rest.repos.listReleases({
owner,
repo,
per_page: 100,
});
// Filters out draft releases
return response.data.filter((release) => !release.draft);
}
// Dependency can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
// Or it can be a string in the form accepted by `getRelease`
async function getReleases(github, dependency) {
if (Array.isArray(dependency)) {
const [dep, fromVersion, toVersion] = dependency;
const upstreamPackageJson = getDependencyPackageJson(dep);
const [owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
const response = await github.rest.repos.listReleases({
owner,
repo,
per_page: 100,
});
const releases = response.data.filter((release) => !release.draft && !release.prerelease);
const unfilteredReleases = await listReleases(github, owner, repo);
// Only include non-draft & non-prerelease releases, unless the to-release is a pre-release, include that one
const releases = unfilteredReleases.filter(
(release) => !release.prerelease || release.tag_name === `v${toVersion}`,
);
const fromVersionIndex = releases.findIndex((release) => release.tag_name === `v${fromVersion}`);
const toVersionIndex = releases.findIndex((release) => release.tag_name === `v${toVersion}`);
@@ -27,14 +35,35 @@ async function getReleases(github, dependency) {
return [await getRelease(github, dependency)];
}
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
// Or it can be a string in the form owner/repo@tag - in this case the tag is used exactly to find the release
// Or it can be a string in the form owner/repo~tag - in this case the latest tag in the same major.minor.patch set is used to find the release
async function getRelease(github, dependency) {
let owner;
let repo;
let tag;
if (dependency.includes("/") && dependency.includes("@")) {
owner = dependency.split("/")[0];
repo = dependency.split("/")[1].split("@")[0];
tag = dependency.split("@")[1];
if (dependency.includes("/")) {
let rest;
[owner, rest] = dependency.split("/")[0];
if (dependency.includes("@")) {
[repo, tag] = rest.split("@");
} else if (dependency.includes("~")) {
[repo, tag] = rest.split("~");
if (tag.includes("-rc.")) {
// If the tag is an RC, find the latest matching RC in the set
try {
const releases = await listReleases(github, owner, repo);
const baseVersion = tag.split("-rc.")[0];
const release = releases.find((release) => release.tag_name.startsWith(baseVersion));
if (release) return release;
} catch (e) {
// Fall back to getReleaseByTag
}
}
}
} else {
const upstreamPackageJson = getDependencyPackageJson(dependency);
[owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
+2 -2
View File
@@ -1379,7 +1379,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
expect(decryptedEvent.getContent().body).toEqual("42");
const exported = await aliceClient.exportRoomKeys();
const exported = await aliceClient.getCrypto()!.exportRoomKeysAsJson();
// start a new client
await aliceClient.stopClient();
@@ -1395,7 +1395,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
keyReceiver = new E2EKeyReceiver(homeserverUrl);
syncResponder = new SyncResponder(homeserverUrl);
await initCrypto(aliceClient);
await aliceClient.importRoomKeys(exported);
await aliceClient.getCrypto()!.importRoomKeysAsJson(exported);
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
+81
View File
@@ -112,6 +112,78 @@ describe("MatrixClient.initRustCrypto", () => {
count: 79,
});
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
"@vdhtest200713:matrix.org": {
KMFSTJSMLB: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "KMFSTJSMLB",
keys: {
"curve25519:KMFSTJSMLB": "LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU",
"ed25519:KMFSTJSMLB": "qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw",
},
user_id: "@vdhtest200713:matrix.org",
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:KMFSTJSMLB":
"aE+PdxLAdwQ/xfJwLmqebvt/lrT97fZas2SQFFrM+dPmHxQtjyS8csm88BLfGRjJKK1B/vWev3AaKqQZwLTUAw",
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k":
"lCd4SA/JT1nnxsgN9yQaLJQhH5hkLMVVx6ba5JAjL1wpWVqyPxzMJHImX6vTztk6S8rybcdfYkea5W/Ii+4HCQ",
},
},
},
},
},
master_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["master"],
keys: {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:MWOGVUTXZN":
"stOu1aHbhsWB/Aj5M/HqBR83QzME+682C995Uc8JxSmmyrlWmgG8QrnoUDG2OFR1t6zNQ+QLEilU4WNEOV73DQ",
},
},
},
},
self_signing_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["self_signing"],
keys: {
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k":
"lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"HKTC7NoBhAkfJtmemmkn/HvCCgBQViWZ0uH7aGPRaWMDFgD8T7Q+y1j3FKZv4mhSopR85Fq3FRyXsG8OVvGeBA",
},
},
},
},
user_signing_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["user_signing"],
keys: {
"ed25519:YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8":
"YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"u8VOi4IaeRJwDgy2ftK02NJQPdBijy8f/0+WnHGG72yfOvMthwWzEw8SrRSNG8glBNrfHinKwCyJJzAJwyepCQ",
},
},
},
},
});
const testStoreName = "test-store";
await populateStore(testStoreName);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
@@ -129,6 +201,15 @@ describe("MatrixClient.initRustCrypto", () => {
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getDeviceVerificationStatus("@vdhtest200713:matrix.org", "KMFSTJSMLB");
// Check that the current device and identity trust is migrated correctly just after migration
expect(verificationStatus).toBeDefined();
expect(verificationStatus!.crossSigningVerified).toEqual(true);
expect(verificationStatus!.signedByOwner).toEqual(true);
// Do some basic checks on the imported data
const deviceKeys = await matrixClient.getCrypto()!.getOwnDeviceKeys();
expect(deviceKeys.curve25519).toEqual("LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU");
+2 -4
View File
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { OidcClientConfig } from "../../src";
import { ValidatedIssuerMetadata } from "../../src/oidc/validate";
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
/**
* Makes a valid OidcClientConfig with minimum valid values
@@ -26,8 +25,7 @@ export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClien
const metadata = mockOpenIdConfiguration(issuer);
return {
issuer,
account: issuer + "account",
accountManagementEndpoint: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
+1 -98
View File
@@ -15,13 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import MockHttpBackend from "matrix-mock-request";
import { AutoDiscoveryAction, M_AUTHENTICATION } from "../../src";
import { AutoDiscoveryAction } from "../../src";
import { AutoDiscovery } from "../../src/autodiscovery";
import { OidcError } from "../../src/oidc/error";
import { makeDelegatedAuthConfig } from "../test-utils/oidc";
// keep to reset the fetch function after using MockHttpBackend
// @ts-ignore private property
@@ -409,10 +406,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -450,10 +443,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -476,9 +465,6 @@ describe("AutoDiscovery", function () {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.authentication": {
invalid: true,
},
});
return Promise.all([
httpBackend.flushAllExpected(),
@@ -494,10 +480,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "FAIL_ERROR",
error: OidcError.Misconfigured,
},
};
expect(conf).toEqual(expected);
@@ -728,10 +710,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: "https://identity.example.org",
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -784,10 +762,6 @@ describe("AutoDiscovery", function () {
"org.example.custom.property": {
cupcakes: "yes",
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -897,75 +871,4 @@ describe("AutoDiscovery", function () {
}),
]);
});
describe("m.authentication", () => {
const homeserverName = "example.org";
const homeserverUrl = "https://chat.example.org/";
const issuer = "https://auth.org/";
beforeAll(() => {
// make these tests independent from fetch mocking above
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
});
beforeEach(() => {
fetchMock.resetBehavior();
fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["v1.5"] });
fetchMock.get("https://example.org/.well-known/matrix/client", {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.authentication": {
issuer,
},
});
});
it("should return valid authentication configuration", async () => {
const config = makeDelegatedAuthConfig(issuer);
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
const result = await AutoDiscovery.findClientConfig(homeserverName);
expect(result[M_AUTHENTICATION.stable!]).toEqual({
state: AutoDiscovery.SUCCESS,
...config,
signingKeys: [],
account: undefined,
error: null,
});
});
it("should set state to error for invalid authentication configuration", async () => {
const config = makeDelegatedAuthConfig(issuer);
// authorization_code is required
config.metadata.grant_types_supported = ["openid"];
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
const result = await AutoDiscovery.findClientConfig(homeserverName);
expect(result[M_AUTHENTICATION.stable!]).toEqual({
state: AutoDiscovery.FAIL_ERROR,
error: OidcError.OpSupport,
});
});
});
});
@@ -0,0 +1,73 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "fake-indexeddb/auto";
import { IndexedDBCryptoStore } from "../../../../src";
import { MigrationState } from "../../../../src/crypto/store/base";
describe("IndexedDBCryptoStore", () => {
describe("Test `existsAndIsNotMigrated`", () => {
beforeEach(async () => {
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
it("Should be true if there is a legacy database", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
await store.startup();
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
expect(result).toBe(true);
});
it("Should be true if there is a legacy database in non migrated state", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
await store.startup();
await store.setMigrationState(MigrationState.NOT_STARTED);
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
expect(result).toBe(true);
});
describe.each([
MigrationState.INITIAL_DATA_MIGRATED,
MigrationState.OLM_SESSIONS_MIGRATED,
MigrationState.MEGOLM_SESSIONS_MIGRATED,
MigrationState.ROOM_SETTINGS_MIGRATED,
])("Exists and Migration state is %s", (migrationState) => {
it("Should be false if migration has started", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
await store.startup();
await store.setMigrationState(migrationState);
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
expect(result).toBe(false);
});
});
it("Should be false if there is no legacy database", async () => {
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
expect(result).toBe(false);
});
});
});
+18
View File
@@ -3012,4 +3012,22 @@ describe("MatrixClient", function () {
expect(result).toEqual({});
});
});
describe("getAuthIssuer", () => {
it("should use unstable prefix", async () => {
httpLookups = [
{
method: "GET",
path: `/auth_issuer`,
data: {
issuer: "https://issuer/",
},
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
];
await expect(client.getAuthIssuer()).resolves.toEqual({ issuer: "https://issuer/" });
expect(httpLookups.length).toEqual(0);
});
});
});
@@ -74,6 +74,13 @@ describe("MatrixRTCSession", () => {
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
});
it("ignores memberships events of members not in the room", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
mockRoom.hasMembershipState = (state) => state === "join";
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships.length).toEqual(0);
});
it("honours created_ts", () => {
const expiredMembership = Object.assign({}, membershipTemplate);
expiredMembership.created_ts = 500;
@@ -91,9 +98,12 @@ describe("MatrixRTCSession", () => {
it("safely ignores events with no memberships section", () => {
const mockRoom = {
...makeMockRoom([]),
roomId: randomString(8),
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue({
on: jest.fn(),
off: jest.fn(),
getStateEvents: (_type: string, _stateKey: string) => [
{
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
@@ -112,9 +122,12 @@ describe("MatrixRTCSession", () => {
it("safely ignores events with junk memberships section", () => {
const mockRoom = {
...makeMockRoom([]),
roomId: randomString(8),
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue({
on: jest.fn(),
off: jest.fn(),
getStateEvents: (_type: string, _stateKey: string) => [
{
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
@@ -87,7 +87,7 @@ describe("MatrixRTCSessionManager", () => {
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
it("Calls onCallEncryption on encryption keys event", () => {
it("Calls onCallEncryption on encryption keys event", async () => {
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
@@ -95,7 +95,7 @@ describe("MatrixRTCSessionManager", () => {
client.emit(ClientEvent.Room, room1);
const onCallEncryptionMock = jest.fn();
client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock;
client.decryptEventIfNeeded = () => Promise.resolve();
const timelineEvent = {
getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix),
getContent: jest.fn().mockReturnValue({}),
@@ -106,6 +106,7 @@ describe("MatrixRTCSessionManager", () => {
},
} as unknown as MatrixEvent;
client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
await new Promise(process.nextTick);
expect(onCallEncryptionMock).toHaveBeenCalled();
});
});
+3
View File
@@ -24,6 +24,7 @@ export function makeMockRoom(memberships: CallMembershipData[], localAge: number
const roomState = makeMockRoomState(memberships, roomId, localAge);
return {
roomId: roomId,
hasMembershipState: jest.fn().mockReturnValue(true),
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue(roomState),
}),
@@ -33,6 +34,8 @@ export function makeMockRoom(memberships: CallMembershipData[], localAge: number
export function makeMockRoomState(memberships: CallMembershipData[], roomId: string, localAge: number | null = null) {
const event = mockRTCEvent(memberships, roomId, localAge);
return {
on: jest.fn(),
off: jest.fn(),
getStateEvents: (_: string, stateKey: string) => {
if (stateKey !== undefined) return event;
return [event];
+23 -5
View File
@@ -20,7 +20,10 @@ limitations under the License.
import fetchMock from "fetch-mock-jest";
import { mocked } from "jest-mock";
import jwtDecode from "jwt-decode";
import { jwtDecode } from "jwt-decode";
import { Crypto } from "@peculiar/webcrypto";
import { getRandomValues } from "node:crypto";
import { TextEncoder } from "node:util";
import { Method } from "../../../src";
import * as crypto from "../../../src/crypto/crypto";
@@ -36,13 +39,15 @@ import { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../test-uti
jest.mock("jwt-decode");
const webCrypto = new Crypto();
// save for resetting mocks
const realSubtleCrypto = crypto.subtleCrypto;
describe("oidc authorization", () => {
const delegatedAuthConfig = makeDelegatedAuthConfig();
const authorizationEndpoint = delegatedAuthConfig.metadata.authorization_endpoint;
const tokenEndpoint = delegatedAuthConfig.metadata.token_endpoint;
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
const clientId = "xyz789";
const baseUrl = "https://test.com";
@@ -53,7 +58,19 @@ describe("oidc authorization", () => {
jest.spyOn(logger, "warn");
jest.setSystemTime(now);
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
fetchMock.get(
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
mockOpenIdConfiguration(),
);
Object.defineProperty(window, "crypto", {
value: {
getRandomValues,
randomUUID: jest.fn().mockReturnValue("not-random-uuid"),
subtle: webCrypto.subtle,
},
});
global.TextEncoder = TextEncoder;
});
afterEach(() => {
@@ -165,7 +182,8 @@ describe("oidc authorization", () => {
token_type: "Bearer",
access_token: "test_access_token",
refresh_token: "test_refresh_token",
id_token: "valid.id.token",
id_token:
"eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJleHAiOjE3MDgzNTY3NjcsInN1YiI6IjAxSFBQMkZTQllERTlQOUVNTThERDdXWkhSIiwiYXVkIjoiMDFIUTBXSDUyV0paV1JSU1k5V0VFUTVUMlEiLCJub25jZSI6ImhScEI2cGtFMDYiLCJhdXRoX3RpbWUiOjE3MDc5OTAzMTIsImlhdCI6MTcwODM1MzE2NywiYXRfaGFzaCI6Il9HSEU4cDhocHFnMW1ac041YUlycVEiLCJpc3MiOiJodHRwczovL2F1dGgtb2lkYy5sYWIuZWxlbWVudC5kZXYvIiwiY19oYXNoIjoib2hJRmNuaUZWd3pGSzVJdXlsX1RlQSJ9.SGUG78dCC3sTWgQBDTicKwamKiPpb6REiz79CM2ml_kVJCoS7gT0TlztC4h25FKi3c9aB3XCVn9J8UzvJgvG8Rt_oS--FIuhK6oRm7NdcN0bCkbG7iZEWGxx-kQnifcCFHyZ6T1CxR8X00Uvc6_lRfBZVlTyuuQaJ_PHiiKMlV93FbxvQUIq6FTkQP2Z56p4JIXIzjOONzA91skTqQGycl5f9Vhp6cqXFzl6ARK30M7A-8UI5qCxClUJ7kD9KgN5YZ7uivLp1x01WBnik2DXH0eSwXcTX2WLkYtMXgMxylJhIiO586apIC5nr7sfip-Y_4PgBlSjRRgrmOGC-VUFCA",
expires_in: 300,
};
+31 -22
View File
@@ -17,39 +17,48 @@ limitations under the License.
import fetchMockJest from "fetch-mock-jest";
import { OidcError } from "../../../src/oidc/error";
import { registerOidcClient } from "../../../src/oidc/register";
import { OidcRegistrationClientMetadata, registerOidcClient } from "../../../src/oidc/register";
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
describe("registerOidcClient()", () => {
const issuer = "https://auth.com/";
const registrationEndpoint = "https://auth.com/register";
const clientName = "Element";
const baseUrl = "https://just.testing";
const metadata: OidcRegistrationClientMetadata = {
clientUri: baseUrl,
redirectUris: [baseUrl],
clientName,
applicationType: "web",
tosUri: "http://tos-uri",
policyUri: "http://policy-uri",
contacts: ["admin@example.com"],
};
const dynamicClientId = "xyz789";
const delegatedAuthConfig = {
issuer,
registrationEndpoint,
authorizationEndpoint: issuer + "auth",
tokenEndpoint: issuer + "token",
};
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
beforeEach(() => {
fetchMockJest.mockClear();
fetchMockJest.resetBehavior();
});
it("should make correct request to register client", async () => {
fetchMockJest.post(registrationEndpoint, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 200,
body: JSON.stringify({ client_id: dynamicClientId }),
});
expect(await registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).toEqual(dynamicClientId);
expect(fetchMockJest).toHaveBeenCalledWith(registrationEndpoint, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
expect(fetchMockJest).toHaveBeenCalledWith(
delegatedAuthConfig.registrationEndpoint!,
expect.objectContaining({
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
method: "POST",
}),
);
expect(JSON.parse(fetchMockJest.mock.calls[0][1]!.body as string)).toEqual(
expect.objectContaining({
client_name: clientName,
client_uri: baseUrl,
response_types: ["code"],
@@ -59,25 +68,25 @@ describe("registerOidcClient()", () => {
token_endpoint_auth_method: "none",
application_type: "web",
}),
});
);
});
it("should throw when registration request fails", async () => {
fetchMockJest.post(registrationEndpoint, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 500,
});
await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
OidcError.DynamicRegistrationFailed,
);
});
it("should throw when registration response is invalid", async () => {
fetchMockJest.post(registrationEndpoint, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 200,
// no clientId in response
body: "{}",
});
await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
OidcError.DynamicRegistrationInvalid,
);
});
+16 -16
View File
@@ -64,7 +64,7 @@ describe("OidcTokenRefresher", () => {
keys: [],
});
fetchMock.post(config.metadata.token_endpoint, {
fetchMock.post(config.tokenEndpoint, {
status: 200,
headers: {
"Content-Type": "application/json",
@@ -88,7 +88,7 @@ describe("OidcTokenRefresher", () => {
},
{ overwriteRoutes: true },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await expect(refresher.oidcClientReady).rejects.toThrow();
expect(logger.error).toHaveBeenCalledWith(
"Failed to initialise OIDC client.",
@@ -98,7 +98,7 @@ describe("OidcTokenRefresher", () => {
});
it("initialises oidc client", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// @ts-ignore peek at private property to see we initialised the client correctly
@@ -114,19 +114,19 @@ describe("OidcTokenRefresher", () => {
describe("doRefreshAccessToken()", () => {
it("should throw when oidcClient has not been initialised", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow(
"Cannot get new token before OIDC client is initialised.",
);
});
it("should refresh the tokens", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
const result = await refresher.doRefreshAccessToken("refresh-token");
expect(fetchMock).toHaveFetched(config.metadata.token_endpoint, {
expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
method: "POST",
});
@@ -137,7 +137,7 @@ describe("OidcTokenRefresher", () => {
});
it("should persist the new tokens", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// spy on our stub
jest.spyOn(refresher, "persistTokens");
@@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => {
it("should only have one inflight refresh request at once", async () => {
fetchMock
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 200,
headers: {
@@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 200,
headers: {
@@ -175,7 +175,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: false },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// reset call counts
fetchMock.resetHistory();
@@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => {
const result2 = await first;
// only one call to token endpoint
expect(fetchMock).toHaveFetchedTimes(1, config.metadata.token_endpoint);
expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint);
expect(result1).toEqual({
accessToken: "first-new-access-token",
refreshToken: "first-new-refresh-token",
@@ -208,7 +208,7 @@ describe("OidcTokenRefresher", () => {
it("should log and rethrow when token refresh fails", async () => {
fetchMock.post(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 503,
headers: {
@@ -218,7 +218,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
await expect(refresher.doRefreshAccessToken("refresh-token")).rejects.toThrow();
@@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => {
// make sure inflight request is cleared after a failure
fetchMock
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 503,
headers: {
@@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 200,
headers: {
@@ -249,7 +249,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: false },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// reset call counts
fetchMock.resetHistory();
+10 -95
View File
@@ -15,107 +15,14 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
import jwtDecode from "jwt-decode";
import { jwtDecode } from "jwt-decode";
import { M_AUTHENTICATION } from "../../../src";
import { logger } from "../../../src/logger";
import {
validateIdToken,
validateOIDCIssuerWellKnown,
validateWellKnownAuthentication,
} from "../../../src/oidc/validate";
import { validateIdToken, validateOIDCIssuerWellKnown } from "../../../src/oidc/validate";
import { OidcError } from "../../../src/oidc/error";
jest.mock("jwt-decode");
describe("validateWellKnownAuthentication()", () => {
const baseWk = {
"m.homeserver": {
base_url: "https://hs.org",
},
};
it("should throw not supported error when wellKnown has no m.authentication section", () => {
expect(() => validateWellKnownAuthentication(undefined)).toThrow(OidcError.NotSupported);
});
it("should throw misconfigured error when authentication issuer is not a string", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: { url: "test.com" },
},
};
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
OidcError.Misconfigured,
);
});
it("should throw misconfigured error when authentication account is not a string", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
account: { url: "test" },
},
};
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
OidcError.Misconfigured,
);
});
it("should throw misconfigured error when authentication account is false", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
account: false,
},
};
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
OidcError.Misconfigured,
);
});
it("should return valid config when wk uses stable m.authentication", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
account: "account.com",
},
};
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
issuer: "test.com",
account: "account.com",
});
});
it("should return valid config when m.authentication account is missing", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
},
};
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
issuer: "test.com",
});
});
it("should remove unexpected properties", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
somethingElse: "test",
},
};
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
issuer: "test.com",
});
});
});
describe("validateOIDCIssuerWellKnown", () => {
const validWk: any = {
authorization_endpoint: "https://test.org/authorize",
@@ -125,6 +32,8 @@ describe("validateOIDCIssuerWellKnown", () => {
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
code_challenge_methods_supported: ["S256"],
account_management_uri: "https://authorize.org/account",
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
};
beforeEach(() => {
// stub to avoid console litter
@@ -157,6 +66,8 @@ describe("validateOIDCIssuerWellKnown", () => {
authorizationEndpoint: validWk.authorization_endpoint,
tokenEndpoint: validWk.token_endpoint,
registrationEndpoint: validWk.registration_endpoint,
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
accountManagementEndpoint: "https://authorize.org/account",
});
});
@@ -167,6 +78,8 @@ describe("validateOIDCIssuerWellKnown", () => {
authorizationEndpoint: validWk.authorization_endpoint,
tokenEndpoint: validWk.token_endpoint,
registrationEndpoint: undefined,
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
accountManagementEndpoint: "https://authorize.org/account",
});
});
@@ -186,6 +99,8 @@ describe("validateOIDCIssuerWellKnown", () => {
["code_challenge_methods_supported", undefined],
["code_challenge_methods_supported", "not an array"],
["code_challenge_methods_supported", ["doesnt include S256"]],
["account_management_uri", { not: "a string" }],
["account_management_actions_supported", { not: "an array" }],
])("should throw OP support error when %s is %s", (key, value) => {
const wk = {
...validWk,
@@ -27,8 +27,9 @@ import {
UploadSigningKeysRequest,
ToDeviceRequest,
} from "@matrix-org/matrix-sdk-crypto-wasm";
import fetchMock from "fetch-mock-jest";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { TypedEventEmitter } from "../../../src";
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, MatrixHttpApi, UIAuthCallback } from "../../../src";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { defer } from "../../../src/utils";
@@ -274,4 +275,302 @@ describe("OutgoingRequestProcessor", () => {
// ... and `makeOutgoingRequest` resolves satisfactorily
await result;
});
describe("Should retry requests", () => {
beforeEach(() => {
jest.useFakeTimers();
// here we use another httpApi instance in order to use fetchMock
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
baseUrl: "https://example.com",
prefix: "/_matrix",
onlyData: true,
});
processor = new OutgoingRequestProcessor(olmMachine, httpApi);
});
afterEach(() => {
jest.useRealTimers();
fetchMock.reset();
});
describe("Should retry on retryable errors", () => {
const retryableErrors: Array<[number, { status: number; body: { error: string } }]> = [
[429, { status: 429, body: { error: "Too Many Requests" } }],
[500, { status: 500, body: { error: "Internal Server Error" } }],
[502, { status: 502, body: { error: "Bad Gateway" } }],
[503, { status: 503, body: { error: "Service Unavailable" } }],
[504, { status: 504, body: { error: "Gateway timeout" } }],
[505, { status: 505, body: { error: "HTTP Version Not Supported" } }],
[506, { status: 506, body: { error: "Variant Also Negotiates" } }],
[507, { status: 507, body: { error: "Insufficient Storage" } }],
[508, { status: 508, body: { error: "Loop Detected" } }],
[510, { status: 510, body: { error: "Not Extended" } }],
[511, { status: 511, body: { error: "Network Authentication Required" } }],
[525, { status: 525, body: { error: "SSL Handshake Failed" } }],
];
describe.each(retryableErrors)(`When status code is %s`, (_, error) => {
test.each(tests)(`for request of type %ss`, async (_, RequestClass, expectedMethod, expectedPath) => {
// first, mock up a request as we might expect to receive it from the Rust layer ...
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new RequestClass("1234", testBody);
fetchMock.mock(expectedPath, error, { method: expectedMethod });
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
// Run all timers and wait for the request promise to resolve/reject
await Promise.all([jest.runAllTimersAsync(), requestPromise.catch(() => {})]);
await expect(requestPromise).rejects.toThrow();
// Should have ultimately made 5 requests (1 initial + 4 retries)
const calls = fetchMock.calls(expectedPath);
expect(calls).toHaveLength(5);
// The promise should have been rejected
await expect(requestPromise).rejects.toThrow();
});
});
});
it("should not retry if M_TOO_LARGE", async () => {
const testBody = '{ "messages": { "user": {"device": "bar" }}}';
const outgoingRequest = new ToDeviceRequest("1234", "custom.type", "12345", testBody);
fetchMock.put("express:/_matrix/client/v3/sendToDevice/:type/:txnId", {
status: 502,
body: {
errcode: "M_TOO_LARGE",
error: "Request too large",
},
});
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
await Promise.all([requestPromise.catch(() => {}), jest.runAllTimersAsync()]);
await expect(requestPromise).rejects.toThrow();
const calls = fetchMock.calls("express:/_matrix/client/v3/sendToDevice/:type/:txnId");
expect(calls).toHaveLength(1);
// The promise should have been rejected
await expect(requestPromise).rejects.toThrow();
});
it("should retry on Failed to fetch connection errors", async () => {
let callCount = 0;
fetchMock.post("path:/_matrix/client/v3/keys/upload", (url, opts) => {
callCount++;
if (callCount == 2) {
return {
status: 200,
body: "{}",
};
} else {
throw new Error("Failed to fetch");
}
});
const outgoingRequest = new KeysUploadRequest("1234", "{}");
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
expect(calls).toHaveLength(2);
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
});
it("should retry to send to-device", async () => {
let callCount = 0;
const testBody = '{ "messages": { "user": {"device": "bar" }}}';
const outgoingRequest = new ToDeviceRequest("1234", "custom.type", "12345", testBody);
fetchMock.put("express:/_matrix/client/v3/sendToDevice/:type/:txnId", (url, opts) => {
callCount++;
if (callCount == 2) {
return {
status: 200,
body: "{}",
};
} else {
throw new Error("Failed to fetch");
}
});
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
const calls = fetchMock.calls("express:/_matrix/client/v3/sendToDevice/:type/:txnId");
expect(calls).toHaveLength(2);
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
});
it("should retry to call with UIA", async () => {
let callCount = 0;
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new UploadSigningKeysRequest(testBody);
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", (url, opts) => {
callCount++;
if (callCount == 2) {
return {
status: 200,
body: "{}",
};
} else {
throw new Error("Failed to fetch");
}
});
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
return await makeRequest({ type: "test" });
};
const requestPromise = processor.makeOutgoingRequest(outgoingRequest, authCallback);
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/device_signing/upload");
expect(calls).toHaveLength(2);
// Will not mark as sent as it's a UIA request
});
it("should retry on respect server cool down on LIMIT_EXCEEDED", async () => {
const retryAfterMs = 5000;
let callCount = 0;
fetchMock.post("path:/_matrix/client/v3/keys/upload", (url, opts) => {
callCount++;
if (callCount == 2) {
return {
status: 200,
body: "{}",
};
} else {
return {
status: 429,
body: {
errcode: "M_LIMIT_EXCEEDED",
error: "Too many requests",
retry_after_ms: retryAfterMs,
},
};
}
});
const outgoingRequest = new KeysUploadRequest("1234", "{}");
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
// advanced by less than the retryAfterMs
await jest.advanceTimersByTimeAsync(retryAfterMs - 1000);
// should not have made a second request yet
{
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
expect(calls).toHaveLength(1);
}
// advanced by the remaining time
await jest.advanceTimersByTimeAsync(retryAfterMs + 1000);
await requestPromise;
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
expect(calls).toHaveLength(2);
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
});
const nonRetryableErrors: Array<[number, { status: number; body: { errcode: string } }]> = [
[400, { status: 400, body: { errcode: "Bad Request" } }],
[401, { status: 401, body: { errcode: "M_UNKNOWN_TOKEN" } }],
[403, { status: 403, body: { errcode: "M_FORBIDDEN" } }],
];
describe.each(nonRetryableErrors)("Should not retry all sort of errors", (_, error) => {
test.each(tests)("for %ss", async (_, RequestClass, expectedMethod, expectedPath) => {
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new RequestClass("1234", testBody);
// @ts-ignore to avoid having to do if else to switch the method (.put/.post)
fetchMock[expectedMethod.toLowerCase()](expectedPath, {
status: error.status,
body: error.body,
});
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
// Run all timers and wait for the request promise to resolve/reject
await Promise.all([jest.runAllTimersAsync(), requestPromise.catch(() => {})]);
await expect(requestPromise).rejects.toThrow();
// Should have only tried once
const calls = fetchMock.calls(expectedPath);
expect(calls).toHaveLength(1);
await expect(requestPromise).rejects.toThrow();
});
});
describe("Should not retry client timeouts", () => {
test.each(tests)("for %ss", async (_, RequestClass, expectedMethod, expectedPath) => {
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new RequestClass("1234", testBody);
// @ts-ignore to avoid having to do if else to switch the method (.put/.post)
fetchMock[expectedMethod.toLowerCase()](expectedPath, () => {
// This is what a client timeout error will throw
throw new DOMException("The user aborted a request.", "AbortError");
});
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
// Run all timers and wait for the request promise to resolve/reject
await Promise.all([jest.runAllTimersAsync(), requestPromise.catch(() => {})]);
await expect(requestPromise).rejects.toThrow();
// Should have only tried once
const calls = fetchMock.calls(expectedPath);
expect(calls).toHaveLength(1);
await expect(requestPromise).rejects.toThrow();
});
});
describe("Should retry until it works", () => {
it.each([1, 2, 3, 4])("should succeed if the call number %s is ok", async (successfulCall) => {
let callCount = 0;
fetchMock.post("path:/_matrix/client/v3/keys/upload", (url, opts) => {
callCount++;
if (callCount == successfulCall) {
return {
status: 200,
body: "{}",
};
} else {
return {
status: 500,
body: { error: "Internal server error" },
};
}
});
const outgoingRequest = new KeysUploadRequest("1234", "{}");
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
expect(calls).toHaveLength(successfulCall);
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
});
});
});
});
+24 -1
View File
@@ -383,7 +383,7 @@ describe("RustCrypto", () => {
);
});
describe(".importRoomKeys and .exportRoomKeys", () => {
describe("importing and exporting room keys", () => {
let rustCrypto: RustCrypto;
beforeEach(
@@ -416,6 +416,29 @@ describe("RustCrypto", () => {
expect(aSession).toStrictEqual(exportedKey);
});
it("should import and export keys as JSON", async () => {
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
let importTotal = 0;
const opt: ImportRoomKeysOpts = {
progressCallback: (stage) => {
importTotal = stage.total ?? 0;
},
};
await rustCrypto.importRoomKeysAsJson(JSON.stringify(someRoomKeys), opt);
expect(importTotal).toBe(someRoomKeys.length);
const keys: Array<IMegolmSessionData> = JSON.parse(await rustCrypto.exportRoomKeysAsJson());
expect(Array.isArray(keys)).toBeTruthy();
expect(keys.length).toBe(someRoomKeys.length);
const aSession = someRoomKeys[0];
const exportedKey = keys.find((k) => k.session_id == aSession.session_id);
expect(aSession).toStrictEqual(exportedKey);
});
});
describe("call preprocess methods", () => {
+17
View File
@@ -0,0 +1,17 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export type NonEmptyArray<T> = [T, ...T[]];
+1 -125
View File
@@ -15,19 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { SigningKey } from "oidc-client-ts";
import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, IServerVersions, M_AUTHENTICATION } from "./client";
import { IClientWellKnown, IWellKnownConfig, IServerVersions } from "./client";
import { logger } from "./logger";
import { MatrixError, Method, timeoutSignal } from "./http-api";
import { discoverAndValidateAuthenticationConfig } from "./oidc/discovery";
import {
ValidatedIssuerConfig,
ValidatedIssuerMetadata,
validateOIDCIssuerWellKnown,
validateWellKnownAuthentication,
} from "./oidc/validate";
import { OidcError } from "./oidc/error";
import { SUPPORTED_MATRIX_VERSIONS } from "./version-support";
// Dev note: Auto discovery is part of the spec.
@@ -65,26 +55,9 @@ interface AutoDiscoveryState {
}
interface WellKnownConfig extends Omit<IWellKnownConfig, "error">, AutoDiscoveryState {}
/**
* @deprecated in favour of OidcClientConfig
*/
interface DelegatedAuthConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig, AutoDiscoveryState {}
/**
* @experimental
*/
export interface OidcClientConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig {
metadata: ValidatedIssuerMetadata;
signingKeys?: SigningKey[];
}
export interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
"m.homeserver": WellKnownConfig;
"m.identity_server": WellKnownConfig;
/**
* @experimental
*/
"m.authentication"?: (OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState;
}
/**
@@ -318,107 +291,10 @@ export class AutoDiscovery {
}
});
const authConfig = await this.discoverAndValidateAuthenticationConfig(wellknown);
clientConfig[M_AUTHENTICATION.stable!] = authConfig;
// Step 8: Give the config to the caller (finally)
return Promise.resolve(clientConfig);
}
/**
* Validate delegated auth configuration
* @deprecated use discoverAndValidateAuthenticationConfig
* - m.authentication config is present and valid
* - delegated auth issuer openid-configuration is reachable
* - delegated auth issuer openid-configuration is configured correctly for us
* When successful, DelegatedAuthConfig will be returned with endpoints used for delegated auth
* Any errors are caught, and AutoDiscoveryState returned with error
* @param wellKnown - configuration object as returned
* by the .well-known auto-discovery endpoint
* @returns Config or failure result
*/
public static async validateDiscoveryAuthenticationConfig(
wellKnown: IClientWellKnown,
): Promise<DelegatedAuthConfig | AutoDiscoveryState> {
try {
const authentication = M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown) || undefined;
const homeserverAuthenticationConfig = validateWellKnownAuthentication(authentication);
const issuerOpenIdConfigUrl = `${this.sanitizeWellKnownUrl(
homeserverAuthenticationConfig.issuer,
)}/.well-known/openid-configuration`;
const issuerWellKnown = await this.fetchWellKnownObject<unknown>(issuerOpenIdConfigUrl);
if (issuerWellKnown.action !== AutoDiscoveryAction.SUCCESS) {
logger.error("Failed to fetch issuer openid configuration");
throw new Error(OidcError.General);
}
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown.raw);
const delegatedAuthConfig: DelegatedAuthConfig = {
state: AutoDiscoveryAction.SUCCESS,
error: null,
...homeserverAuthenticationConfig,
...validatedIssuerConfig,
};
return delegatedAuthConfig;
} catch (error) {
const errorMessage = (error as Error).message as unknown as OidcError;
const errorType = Object.values(OidcError).includes(errorMessage) ? errorMessage : OidcError.General;
const state =
errorType === OidcError.NotSupported ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR;
return {
state,
error: errorType,
};
}
}
/**
* Validate delegated auth configuration
* - m.authentication config is present and valid
* - delegated auth issuer openid-configuration is reachable
* - delegated auth issuer openid-configuration is configured correctly for us
* When successful, validated authentication metadata and optionally signing keys will be returned
* Any errors are caught, and AutoDiscoveryState returned with error
* @param wellKnown - configuration object as returned
* by the .well-known auto-discovery endpoint
* @returns Config or failure result
*/
public static async discoverAndValidateAuthenticationConfig(
wellKnown: IClientWellKnown,
): Promise<(OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState> {
try {
const authentication = M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown) || undefined;
const result = await discoverAndValidateAuthenticationConfig(authentication);
// include this for backwards compatibility
const validatedIssuerConfig = validateOIDCIssuerWellKnown(result.metadata);
const response = {
state: AutoDiscoveryAction.SUCCESS,
error: null,
...validatedIssuerConfig,
...result,
};
return response;
} catch (error) {
const errorMessage = (error as Error).message as unknown as OidcError;
const errorType = Object.values(OidcError).includes(errorMessage) ? errorMessage : OidcError.General;
const state =
errorType === OidcError.NotSupported ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR;
return {
state,
error: errorType,
};
}
}
/**
* Attempts to automatically discover client configuration information
* prior to logging in. Such information includes the homeserver URL
+14 -11
View File
@@ -625,13 +625,10 @@ export interface IServerVersions {
unstable_features: Record<string, boolean>;
}
export const M_AUTHENTICATION = new UnstableValue("m.authentication", "org.matrix.msc2965.authentication");
export interface IClientWellKnown {
[key: string]: any;
"m.homeserver"?: IWellKnownConfig;
"m.identity_server"?: IWellKnownConfig;
[M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965
}
export interface IWellKnownConfig<T = IClientWellKnown> {
@@ -645,14 +642,6 @@ export interface IWellKnownConfig<T = IClientWellKnown> {
server_name?: string;
}
export interface IDelegatedAuthConfig {
// MSC2965
/** The OIDC Provider/issuer the client should use */
issuer: string;
/** The optional URL of the web UI where the user can manage their account */
account?: string;
}
interface IKeyBackupPath {
path: string;
queryData?: {
@@ -9974,6 +9963,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw err;
}
}
/**
* Get the OIDC issuer responsible for authentication on this server, if any
* @returns Resolves: A promise of an object containing the OIDC issuer if configured
* @returns Rejects: when the request fails (module:http-api.MatrixError)
* @experimental - part of MSC2965
*/
public async getAuthIssuer(): Promise<{
issuer: string;
}> {
return this.http.request(Method.Get, "/auth_issuer", undefined, undefined, {
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
});
}
}
/**
+26 -2
View File
@@ -96,6 +96,17 @@ export interface CryptoApi {
*/
exportRoomKeys(): Promise<IMegolmSessionData[]>;
/**
* Get a JSON list containing all of the room keys
*
* This should be encrypted before returning it to the user.
*
* @returns a promise which resolves to a JSON string
* encoding a list of session export objects,
* each of which is an IMegolmSessionData
*/
exportRoomKeysAsJson(): Promise<string>;
/**
* Import a list of room keys previously exported by exportRoomKeys
*
@@ -105,6 +116,17 @@ export interface CryptoApi {
*/
importRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void>;
/**
* Import a JSON string encoding a list of room keys previously
* exported by exportRoomKeysAsJson
*
* @param keys - a JSON string encoding a list of session export
* objects, each of which is an IMegolmSessionData
* @param opts - options object
* @returns a promise which resolves once the keys have been imported
*/
importRoomKeysAsJson(keys: string, opts?: ImportRoomKeysOpts): Promise<void>;
/**
* Check if the given user has published cross-signing keys.
*
@@ -593,7 +615,8 @@ export class DeviceVerificationStatus {
/**
* Room key import progress report.
* Used when calling {@link CryptoApi#importRoomKeys} as the parameter of
* Used when calling {@link CryptoApi#importRoomKeys} or
* {@link CryptoApi#importRoomKeysAsJson} as the parameter of
* the progressCallback. Used to display feedback.
*/
export interface ImportRoomKeyProgressData {
@@ -604,7 +627,8 @@ export interface ImportRoomKeyProgressData {
}
/**
* Options object for {@link CryptoApi#importRoomKeys}.
* Options object for {@link CryptoApi#importRoomKeys} and
* {@link CryptoApi#importRoomKeysAsJson}.
*/
export interface ImportRoomKeysOpts {
/** Reports ongoing progress of the import process. Can be used for feedback. */
+23
View File
@@ -3142,6 +3142,16 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return exportedSessions;
}
/**
* Get a JSON list containing all of the room keys
*
* @returns a JSON string encoding a list of session
* export objects, each of which is an IMegolmSessionData
*/
public async exportRoomKeysAsJson(): Promise<string> {
return JSON.stringify(await this.exportRoomKeys());
}
/**
* Import a list of room keys previously exported by exportRoomKeys
*
@@ -3184,6 +3194,19 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
).then();
}
/**
* Import a JSON string encoding a list of room keys previously
* exported by exportRoomKeysAsJson
*
* @param keys - a JSON string encoding a list of session export
* objects, each of which is an IMegolmSessionData
* @param opts - options object
* @returns a promise which resolves once the keys have been imported
*/
public async importRoomKeysAsJson(keys: string, opts?: ImportRoomKeysOpts): Promise<void> {
return await this.importRoomKeys(JSON.parse(keys));
}
/**
* Counts the number of end to end session keys that are waiting to be backed up
* @returns Promise which resolves to the number of sessions requiring backup
+11
View File
@@ -306,6 +306,13 @@ export interface ParkedSharedHistory {
forwardingCurve25519KeyChain: string[];
}
/**
* Keys for the `account` object store to store the migration state.
* Values are defined in `MigrationState`.
* @internal
*/
export const ACCOUNT_OBJECT_KEY_MIGRATION_STATE = "migrationState";
/**
* A record of which steps have been completed in the libolm to Rust Crypto migration.
*
@@ -328,6 +335,10 @@ export enum MigrationState {
/** MEGOLM_SESSIONS_MIGRATED, and in addition, we have migrated all the room settings. */
ROOM_SETTINGS_MIGRATED,
/** ROOM_SETTINGS_MIGRATED, and in addition, we have done the first own keys query in order to
* load the public part of the keys that have been migrated */
INITIAL_OWN_KEY_QUERY_DONE,
}
/**
@@ -30,6 +30,7 @@ import {
ParkedSharedHistory,
SecretStorePrivateKeys,
SESSION_BATCH_SIZE,
ACCOUNT_OBJECT_KEY_MIGRATION_STATE,
} from "./base";
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
import { ICrossSigningKey } from "../../client";
@@ -40,9 +41,6 @@ import { IndexedDBCryptoStore } from "./indexeddb-crypto-store";
const PROFILE_TRANSACTIONS = false;
/* Keys for the `account` object store */
const ACCOUNT_OBJECT_KEY_MIGRATION_STATE = "migrationState";
/**
* Implementation of a CryptoStore which is backed by an existing
* IndexedDB connection. Generally you want IndexedDBCryptoStore
@@ -33,6 +33,7 @@ import {
OutgoingRoomKeyRequest,
ParkedSharedHistory,
SecretStorePrivateKeys,
ACCOUNT_OBJECT_KEY_MIGRATION_STATE,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
@@ -63,6 +64,52 @@ export class IndexedDBCryptoStore implements CryptoStore {
return IndexedDBHelpers.exists(indexedDB, dbName);
}
/**
* Utility to check if a legacy crypto store exists and has not been migrated.
* Returns true if the store exists and has not been migrated, false otherwise.
*/
public static existsAndIsNotMigrated(indexedDb: IDBFactory, dbName: string): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
let exists = true;
const openDBRequest = indexedDb.open(dbName);
openDBRequest.onupgradeneeded = (): void => {
// Since we did not provide an explicit version when opening, this event
// should only fire if the DB did not exist before at any version.
exists = false;
};
openDBRequest.onblocked = (): void => reject(openDBRequest.error);
openDBRequest.onsuccess = (): void => {
const db = openDBRequest.result;
if (!exists) {
db.close();
// The DB did not exist before, but has been created as part of this
// existence check. Delete it now to restore previous state. Delete can
// actually take a while to complete in some browsers, so don't wait for
// it. This won't block future open calls that a store might issue next to
// properly set up the DB.
indexedDb.deleteDatabase(dbName);
resolve(false);
} else {
const tx = db.transaction([IndexedDBCryptoStore.STORE_ACCOUNT], "readonly");
const objectStore = tx.objectStore(IndexedDBCryptoStore.STORE_ACCOUNT);
const getReq = objectStore.get(ACCOUNT_OBJECT_KEY_MIGRATION_STATE);
getReq.onsuccess = (): void => {
const migrationState = getReq.result ?? MigrationState.NOT_STARTED;
resolve(migrationState === MigrationState.NOT_STARTED);
};
getReq.onerror = (): void => {
reject(getReq.error);
};
db.close();
}
};
openDBRequest.onerror = (): void => reject(openDBRequest.error);
});
}
private backendPromise?: Promise<CryptoStore>;
private backend?: CryptoStore;
+46
View File
@@ -151,3 +151,49 @@ export async function retryNetworkOperation<T>(maxAttempts: number, callback: ()
}
throw lastConnectionError;
}
/**
* Calculate the backoff time for a request retry attempt.
* This produces wait times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
* failure was due to a rate limited request, the time specified in the error is returned.
*
* Returns -1 if the error is not retryable, or if we reach the maximum number of attempts.
*
* @param err - The error thrown by the http call
* @param attempts - The number of attempts made so far, including the one that just failed.
* @param retryConnectionError - Whether to retry on {@link ConnectionError} (CORS, connection is down, etc.)
*/
export function calculateRetryBackoff(err: any, attempts: number, retryConnectionError: boolean): number {
if (attempts > 4) {
return -1; // give up
}
if (err instanceof ConnectionError && !retryConnectionError) {
return -1;
}
if (err.httpStatus && (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401)) {
// client error; no amount of retrying will save you now.
return -1;
}
if (err.name === "AbortError") {
// this is a client timeout, that is already very high 60s/80s
// we don't want to retry, as it could do it for very long
return -1;
}
// If we are trying to send an event (or similar) that is too large in any way, then retrying won't help
if (err.name === "M_TOO_LARGE") {
return -1;
}
if (err.name === "M_LIMIT_EXCEEDED") {
const waitTime = err.data.retry_after_ms;
if (waitTime > 0) {
return waitTime;
}
}
return 1000 * Math.pow(2, attempts);
}
+1
View File
@@ -56,6 +56,7 @@ export * from "./crypto/store/localStorage-crypto-store";
export * from "./crypto/store/indexeddb-crypto-store";
export type { OutgoingRoomKeyRequest } from "./crypto/store/base";
export * from "./content-repo";
export * from "./@types/common";
export * from "./@types/uia";
export * from "./@types/event";
export * from "./@types/PushRules";
+12 -5
View File
@@ -21,6 +21,7 @@ import { Room } from "../models/room";
import { MatrixClient } from "../client";
import { EventType } from "../@types/event";
import { CallMembership, CallMembershipData } from "./CallMembership";
import { RoomStateEvent } from "../models/room-state";
import { Focus } from "./focus";
import { MatrixError, MatrixEvent } from "../matrix";
import { randomString, secureRandomBase64Url } from "../randomstring";
@@ -152,9 +153,11 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
if (membership.isExpired()) {
logger.info(
`Ignoring expired device membership ${memberEvent.getSender()}/${membership.deviceId}`,
);
logger.info(`Ignoring expired device membership ${membership.sender}/${membership.deviceId}`);
continue;
}
if (!room.hasMembershipState(membership.sender ?? "", "join")) {
logger.info(`Ignoring membership of user ${membership.sender} who is not in the room.`);
continue;
}
callMemberships.push(membership);
@@ -176,7 +179,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
/**
* Return a the MatrixRTC for the room, whether there are currently active members or not
* Return the MatrixRTC session for the room, whether there are currently active members or not
*/
public static roomSessionForRoom(client: MatrixClient, room: Room): MatrixRTCSession {
const callMemberships = MatrixRTCSession.callMembershipsForRoom(room);
@@ -191,6 +194,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
) {
super();
this._callId = memberships[0]?.callId;
const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
roomState?.on(RoomStateEvent.Members, this.onMembershipUpdate);
this.setExpiryTimer();
}
@@ -214,6 +219,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
clearTimeout(this.memberEventTimeout);
this.memberEventTimeout = undefined;
}
const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
roomState?.off(RoomStateEvent.Members, this.onMembershipUpdate);
}
/**
@@ -506,7 +513,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
return;
}
// We currently only handle callId = ""
// We currently only handle callId = "" (which is the default for room scoped calls)
if (callId !== "") {
logger.warn(
`Received m.call.encryption_keys with unsupported callId: userId=${userId}, deviceId=${deviceId}, callId=${callId}`,
+7 -3
View File
@@ -98,16 +98,20 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
return this.roomSessions.get(room.roomId)!;
}
private onTimeline = (event: MatrixEvent): void => {
if (event.getType() !== EventType.CallEncryptionKeysPrefix) return;
private async consumeCallEncryptionEvent(event: MatrixEvent): Promise<void> {
await this.client.decryptEventIfNeeded(event);
if (event.getType() !== EventType.CallEncryptionKeysPrefix) return Promise.resolve();
const room = this.client.getRoom(event.getRoomId());
if (!room) {
logger.error(`Got room state event for unknown room ${event.getRoomId()}!`);
return;
return Promise.resolve();
}
this.getRoomSession(room).onCallEncryption(event);
}
private onTimeline = (event: MatrixEvent): void => {
this.consumeCallEncryptionEvent(event);
};
private onRoom = (room: Room): void => {
+4
View File
@@ -163,6 +163,10 @@ export interface IVisibilityChange {
reason: string | null;
}
export interface IMarkedUnreadEvent {
unread: boolean;
}
export interface IClearEvent {
room_id?: string;
type: string;
+2 -2
View File
@@ -198,8 +198,8 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
};
private async fetchRootEvent(): Promise<void> {
this.rootEvent = this.room.findEventById(this.id);
// If the rootEvent does not exist in the local stores, then fetch it from the server.
// Always fetch the root event, even if we already have it, so we can get the latest
// state (via unsigned).
try {
const eventData = await this.client.fetchRoomEvent(this.roomId, this.id);
const mapper = this.client.getEventMapper();
+7 -4
View File
@@ -16,7 +16,6 @@ limitations under the License.
import { IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts";
import { IDelegatedAuthConfig } from "../client";
import { subtleCrypto, TextEncoder } from "../crypto/crypto";
import { logger } from "../logger";
import { randomString } from "../randomstring";
@@ -129,6 +128,7 @@ export const generateAuthorizationUrl = async (
* @param nonce - state
* @param prompt - indicates to the OP which flow the user should see - eg login or registration
* See https://openid.net/specs/openid-connect-prompt-create-1_0.html#name-prompt-parameter
* @param urlState - value to append to the opaque state identifier to uniquely identify the callback
* @returns a Promise with the url as a string
*/
export const generateOidcAuthorizationUrl = async ({
@@ -139,6 +139,7 @@ export const generateOidcAuthorizationUrl = async ({
identityServerUrl,
nonce,
prompt,
urlState,
}: {
clientId: string;
metadata: ValidatedIssuerMetadata;
@@ -147,8 +148,9 @@ export const generateOidcAuthorizationUrl = async ({
redirectUri: string;
nonce: string;
prompt?: string;
urlState?: string;
}): Promise<string> => {
const scope = await generateScope();
const scope = generateScope();
const oidcClient = new OidcClient({
...metadata,
client_id: clientId,
@@ -164,6 +166,7 @@ export const generateOidcAuthorizationUrl = async ({
state: userState,
nonce,
prompt,
url_state: urlState,
});
return request.url;
@@ -205,7 +208,7 @@ export const completeAuthorizationCodeGrant = async (
code: string,
state: string,
): Promise<{
oidcClientSettings: IDelegatedAuthConfig & { clientId: string };
oidcClientSettings: { clientId: string; issuer: string };
tokenResponse: BearerTokenResponse;
homeserverUrl: string;
idTokenClaims: IdTokenClaims;
@@ -235,7 +238,7 @@ export const completeAuthorizationCodeGrant = async (
// hydrate the sign in state and create a client
// the stored sign in state includes oidc configuration we set at the start of the oidc login flow
const signInState = SigninState.fromStorageString(stateString);
const signInState = await SigninState.fromStorageString(stateString);
const client = new OidcClient({ ...signInState, stateStore });
// validate the code and state, and attempt to swap the code for tokens
+17 -18
View File
@@ -14,36 +14,35 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MetadataService, OidcClientSettingsStore, SigningKey } from "oidc-client-ts";
import { MetadataService, OidcClientSettingsStore } from "oidc-client-ts";
import { IDelegatedAuthConfig } from "../client";
import { isValidatedIssuerMetadata, ValidatedIssuerMetadata, validateWellKnownAuthentication } from "./validate";
import { isValidatedIssuerMetadata, validateOIDCIssuerWellKnown } from "./validate";
import { Method, timeoutSignal } from "../http-api";
import { OidcClientConfig } from "./index";
/**
* @experimental
* Discover and validate delegated auth configuration
* - m.authentication config is present and valid
* - delegated auth issuer openid-configuration is reachable
* - delegated auth issuer openid-configuration is configured correctly for us
* Fetches https://oidc-issuer.example.com/.well-known/openid-configuration and other files linked therein.
* When successful, validated metadata is returned
* @param wellKnown - configuration object as returned
* by the .well-known auto-discovery endpoint
* @param issuer - the OIDC issuer as returned by the /auth_issuer API
* @returns validated authentication metadata and optionally signing keys
* @throws when delegated auth config is invalid or unreachable
*/
export const discoverAndValidateAuthenticationConfig = async (
authenticationConfig?: IDelegatedAuthConfig,
): Promise<
IDelegatedAuthConfig & {
metadata: ValidatedIssuerMetadata;
signingKeys?: SigningKey[];
}
> => {
const homeserverAuthenticationConfig = validateWellKnownAuthentication(authenticationConfig);
export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Promise<OidcClientConfig> => {
const issuerOpenIdConfigUrl = new URL(".well-known/openid-configuration", issuer);
const issuerWellKnownResponse = await fetch(issuerOpenIdConfigUrl, {
method: Method.Get,
signal: timeoutSignal(5000),
});
const issuerWellKnown = await issuerWellKnownResponse.json();
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown);
// create a temporary settings store so we can use metadata service for discovery
// create a temporary settings store, so we can use metadata service for discovery
const settings = new OidcClientSettingsStore({
authority: homeserverAuthenticationConfig.issuer,
authority: issuer,
redirect_uri: "", // Not known yet, this is here to make the type checker happy
client_id: "", // Not known yet, this is here to make the type checker happy
});
@@ -54,7 +53,7 @@ export const discoverAndValidateAuthenticationConfig = async (
isValidatedIssuerMetadata(metadata);
return {
...homeserverAuthenticationConfig,
...validatedIssuerConfig,
metadata,
signingKeys,
};
+12
View File
@@ -14,9 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type { SigningKey } from "oidc-client-ts";
import { ValidatedIssuerConfig, ValidatedIssuerMetadata } from "./validate";
export * from "./authorize";
export * from "./discovery";
export * from "./error";
export * from "./register";
export * from "./tokenRefresher";
export * from "./validate";
/**
* Validated config for native OIDC authentication, as returned by {@link discoverAndValidateOIDCIssuerWellKnown}.
* Contains metadata and signing keys from the issuer's well-known (https://oidc-issuer.example.com/.well-known/openid-configuration).
*/
export interface OidcClientConfig extends ValidatedIssuerConfig {
metadata: ValidatedIssuerMetadata;
signingKeys?: SigningKey[];
}
+36 -21
View File
@@ -14,21 +14,41 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { IDelegatedAuthConfig } from "../client";
import { OidcClientConfig } from ".";
import { OidcError } from "./error";
import { Method } from "../http-api";
import { logger } from "../logger";
import { ValidatedIssuerConfig } from "./validate";
import { NonEmptyArray } from "../@types/common";
/**
* Client metadata passed to registration endpoint
*/
export type OidcRegistrationClientMetadata = {
clientName: string;
clientUri: string;
redirectUris: string[];
clientName: OidcRegistrationRequestBody["client_name"];
clientUri: OidcRegistrationRequestBody["client_uri"];
logoUri?: OidcRegistrationRequestBody["logo_uri"];
applicationType: OidcRegistrationRequestBody["application_type"];
redirectUris: OidcRegistrationRequestBody["redirect_uris"];
contacts: OidcRegistrationRequestBody["contacts"];
tosUri: OidcRegistrationRequestBody["tos_uri"];
policyUri: OidcRegistrationRequestBody["policy_uri"];
};
interface OidcRegistrationRequestBody {
client_name: string;
client_uri: string;
logo_uri?: string;
contacts: NonEmptyArray<string>;
tos_uri: string;
policy_uri: string;
redirect_uris?: NonEmptyArray<string>;
response_types?: NonEmptyArray<string>;
grant_types?: NonEmptyArray<string>;
id_token_signed_response_alg: string;
token_endpoint_auth_method: string;
application_type: "web" | "native";
}
/**
* Make the client registration request
* @param registrationEndpoint - URL as returned from issuer ./well-known/openid-configuration
@@ -42,7 +62,7 @@ const doRegistration = async (
clientMetadata: OidcRegistrationClientMetadata,
): Promise<string> => {
// https://openid.net/specs/openid-connect-registration-1_0.html
const metadata = {
const metadata: OidcRegistrationRequestBody = {
client_name: clientMetadata.clientName,
client_uri: clientMetadata.clientUri,
response_types: ["code"],
@@ -50,7 +70,11 @@ const doRegistration = async (
redirect_uris: clientMetadata.redirectUris,
id_token_signed_response_alg: "RS256",
token_endpoint_auth_method: "none",
application_type: "web",
application_type: clientMetadata.applicationType,
logo_uri: clientMetadata.logoUri,
contacts: clientMetadata.contacts,
policy_uri: clientMetadata.policyUri,
tos_uri: clientMetadata.tosUri,
};
const headers = {
"Accept": "application/json",
@@ -87,26 +111,17 @@ const doRegistration = async (
/**
* Attempts dynamic registration against the configured registration endpoint
* @param delegatedAuthConfig - Auth config from ValidatedServerConfig
* @param clientName - Client name to register with the OP, eg 'Element'
* @param baseUrl - URL of the home page of the Client, eg 'https://app.element.io/'
* @param delegatedAuthConfig - Auth config from {@link discoverAndValidateOIDCIssuerWellKnown}
* @param clientMetadata - The metadata for the client which to register
* @returns Promise<string> resolved with registered clientId
* @throws when registration is not supported, on failed request or invalid response
*/
export const registerOidcClient = async (
delegatedAuthConfig: IDelegatedAuthConfig & ValidatedIssuerConfig,
clientName: string,
baseUrl: string,
delegatedAuthConfig: OidcClientConfig,
clientMetadata: OidcRegistrationClientMetadata,
): Promise<string> => {
const clientMetadata = {
clientName,
clientUri: baseUrl,
redirectUris: [baseUrl],
};
if (!delegatedAuthConfig.registrationEndpoint) {
throw new Error(OidcError.DynamicRegistrationNotSupported);
}
const clientId = await doRegistration(delegatedAuthConfig.registrationEndpoint, clientMetadata);
return clientId;
return doRegistration(delegatedAuthConfig.registrationEndpoint, clientMetadata);
};
+6 -7
View File
@@ -17,9 +17,8 @@ limitations under the License.
import { IdTokenClaims, OidcClient, WebStorageStateStore } from "oidc-client-ts";
import { AccessTokens } from "../http-api";
import { IDelegatedAuthConfig } from "../client";
import { generateScope } from "./authorize";
import { discoverAndValidateAuthenticationConfig } from "./discovery";
import { discoverAndValidateOIDCIssuerWellKnown } from "./discovery";
import { logger } from "../logger";
/**
@@ -42,9 +41,9 @@ export class OidcTokenRefresher {
public constructor(
/**
* Delegated auth config as found in matrix client .well-known
* The OIDC issuer as returned by the /auth_issuer API
*/
authConfig: IDelegatedAuthConfig,
issuer: string,
/**
* id of this client as registered with the OP
*/
@@ -63,17 +62,17 @@ export class OidcTokenRefresher {
*/
private readonly idTokenClaims: IdTokenClaims,
) {
this.oidcClientReady = this.initialiseOidcClient(authConfig, clientId, deviceId, redirectUri);
this.oidcClientReady = this.initialiseOidcClient(issuer, clientId, deviceId, redirectUri);
}
private async initialiseOidcClient(
authConfig: IDelegatedAuthConfig,
issuer: string,
clientId: string,
deviceId: string,
redirectUri: string,
): Promise<void> {
try {
const config = await discoverAndValidateAuthenticationConfig(authConfig);
const config = await discoverAndValidateOIDCIssuerWellKnown(issuer);
const scope = generateScope(deviceId);
+26 -32
View File
@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import jwtDecode from "jwt-decode";
import { jwtDecode } from "jwt-decode";
import { OidcMetadata, SigninResponse } from "oidc-client-ts";
import { IDelegatedAuthConfig } from "../client";
import { logger } from "../logger";
import { OidcError } from "./error";
@@ -31,31 +30,8 @@ export type ValidatedIssuerConfig = {
authorizationEndpoint: string;
tokenEndpoint: string;
registrationEndpoint?: string;
};
/**
* Validates MSC2965 m.authentication config
* Returns valid configuration
* @param wellKnown - client well known as returned from ./well-known/client/matrix
* @returns config - when present and valid
* @throws when config is not found or invalid
*/
export const validateWellKnownAuthentication = (authentication?: IDelegatedAuthConfig): IDelegatedAuthConfig => {
if (!authentication) {
throw new Error(OidcError.NotSupported);
}
if (
typeof authentication.issuer === "string" &&
(!authentication.hasOwnProperty("account") || typeof authentication.account === "string")
) {
return {
issuer: authentication.issuer,
account: authentication.account,
};
}
throw new Error(OidcError.Misconfigured);
accountManagementEndpoint?: string;
accountManagementActionsSupported?: string[];
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
@@ -74,6 +50,16 @@ const optionalStringProperty = (wellKnown: Record<string, unknown>, key: string)
}
return true;
};
const optionalStringArrayProperty = (wellKnown: Record<string, unknown>, key: string): boolean => {
if (
!!wellKnown[key] &&
(!Array.isArray(wellKnown[key]) || !(<unknown[]>wellKnown[key]).every((v) => typeof v === "string"))
) {
logger.error(`Invalid property: ${key}`);
return false;
}
return true;
};
const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, value: any): boolean => {
const array = wellKnown[key];
if (!array || !Array.isArray(array) || !array.includes(value)) {
@@ -102,6 +88,8 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer
requiredStringProperty(wellKnown, "token_endpoint"),
requiredStringProperty(wellKnown, "revocation_endpoint"),
optionalStringProperty(wellKnown, "registration_endpoint"),
optionalStringProperty(wellKnown, "account_management_uri"),
optionalStringArrayProperty(wellKnown, "account_management_actions_supported"),
requiredArrayValue(wellKnown, "response_types_supported", "code"),
requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"),
requiredArrayValue(wellKnown, "code_challenge_methods_supported", "S256"),
@@ -109,10 +97,12 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer
if (!isInvalid) {
return {
authorizationEndpoint: wellKnown["authorization_endpoint"],
tokenEndpoint: wellKnown["token_endpoint"],
registrationEndpoint: wellKnown["registration_endpoint"],
} as ValidatedIssuerConfig;
authorizationEndpoint: <string>wellKnown["authorization_endpoint"],
tokenEndpoint: <string>wellKnown["token_endpoint"],
registrationEndpoint: <string>wellKnown["registration_endpoint"],
accountManagementEndpoint: <string>wellKnown["account_management_uri"],
accountManagementActionsSupported: <string[]>wellKnown["account_management_actions_supported"],
};
}
logger.error("Issuer configuration not valid");
@@ -134,7 +124,11 @@ export type ValidatedIssuerMetadata = Partial<OidcMetadata> &
| "response_types_supported"
| "grant_types_supported"
| "code_challenge_methods_supported"
>;
> & {
// MSC2965 extensions to the OIDC spec
account_management_uri?: string;
account_management_actions_supported?: string[];
};
/**
* Wraps validateOIDCIssuerWellKnown in a type assertion
+37 -12
View File
@@ -15,11 +15,11 @@ limitations under the License.
*/
import {
OlmMachine,
KeysBackupRequest,
KeysClaimRequest,
KeysQueryRequest,
KeysUploadRequest,
OlmMachine,
RoomMessageRequest,
SignatureUploadRequest,
ToDeviceRequest,
@@ -27,8 +27,8 @@ import {
} from "@matrix-org/matrix-sdk-crypto-wasm";
import { logger } from "../logger";
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
import { logDuration, QueryDict } from "../utils";
import { calculateRetryBackoff, IHttpOpts, MatrixHttpApi, Method } from "../http-api";
import { logDuration, QueryDict, sleep } from "../utils";
import { IAuthDict, UIAuthCallback } from "../interactive-auth";
import { UIAResponse } from "../@types/uia";
import { ToDeviceMessageId } from "../@types/event";
@@ -71,15 +71,15 @@ export class OutgoingRequestProcessor {
* for the complete list of request types
*/
if (msg instanceof KeysUploadRequest) {
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body);
resp = await this.requestWithRetry(Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body);
} else if (msg instanceof KeysQueryRequest) {
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body);
resp = await this.requestWithRetry(Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body);
} else if (msg instanceof KeysClaimRequest) {
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body);
resp = await this.requestWithRetry(Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body);
} else if (msg instanceof SignatureUploadRequest) {
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body);
resp = await this.requestWithRetry(Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body);
} else if (msg instanceof KeysBackupRequest) {
resp = await this.rawJsonRequest(
resp = await this.requestWithRetry(
Method.Put,
"/_matrix/client/v3/room_keys/keys",
{ version: msg.version },
@@ -91,7 +91,7 @@ export class OutgoingRequestProcessor {
const path =
`/_matrix/client/v3/rooms/${encodeURIComponent(msg.room_id)}/send/` +
`${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`;
resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
resp = await this.requestWithRetry(Method.Put, path, {}, msg.body);
} else if (msg instanceof UploadSigningKeysRequest) {
await this.makeRequestWithUIA(
Method.Post,
@@ -154,7 +154,7 @@ export class OutgoingRequestProcessor {
const path =
`/_matrix/client/v3/sendToDevice/${encodeURIComponent(request.event_type)}/` +
encodeURIComponent(request.txn_id);
return await this.rawJsonRequest(Method.Put, path, {}, request.body);
return await this.requestWithRetry(Method.Put, path, {}, request.body);
}
private async makeRequestWithUIA<T>(
@@ -165,7 +165,7 @@ export class OutgoingRequestProcessor {
uiaCallback: UIAuthCallback<T> | undefined,
): Promise<string> {
if (!uiaCallback) {
return await this.rawJsonRequest(method, path, queryParams, body);
return await this.requestWithRetry(method, path, queryParams, body);
}
const parsedBody = JSON.parse(body);
@@ -176,7 +176,7 @@ export class OutgoingRequestProcessor {
if (auth !== null) {
newBody.auth = auth;
}
const resp = await this.rawJsonRequest(method, path, queryParams, JSON.stringify(newBody));
const resp = await this.requestWithRetry(method, path, queryParams, JSON.stringify(newBody));
return JSON.parse(resp) as T;
};
@@ -184,6 +184,31 @@ export class OutgoingRequestProcessor {
return JSON.stringify(resp);
}
private async requestWithRetry(
method: Method,
path: string,
queryParams: QueryDict,
body: string,
): Promise<string> {
let currentRetryCount = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
try {
return await this.rawJsonRequest(method, path, queryParams, body);
} catch (e) {
currentRetryCount++;
const backoff = calculateRetryBackoff(e, currentRetryCount, true);
if (backoff < 0) {
// Max number of retries reached, or error is not retryable. rethrow the error
throw e;
}
// wait for the specified time and then retry the request
await sleep(backoff);
}
}
}
private async rawJsonRequest(method: Method, path: string, queryParams: QueryDict, body: string): Promise<string> {
const opts = {
// inhibit the JSON stringification and parsing within HttpApi.
+12 -1
View File
@@ -188,7 +188,18 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
* @returns a promise which resolves once the keys have been imported
*/
public async importRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void> {
const jsonKeys = JSON.stringify(keys);
await this.importRoomKeysAsJson(JSON.stringify(keys), opts);
}
/**
* Import a list of room keys previously exported by exportRoomKeysAsJson
*
* @param keys - a JSON string encoding a list of session export objects,
* each of which is an IMegolmSessionData
* @param opts - options object
* @returns a promise which resolves once the keys have been imported
*/
public async importRoomKeysAsJson(jsonKeys: string, opts?: ImportRoomKeysOpts): Promise<void> {
await this.olmMachine.importExportedRoomKeys(jsonKeys, (progress: BigInt, total: BigInt): void => {
const importOpt: ImportRoomKeyProgressData = {
total: Number(total),
+20 -1
View File
@@ -22,7 +22,7 @@ import { IHttpOpts, MatrixHttpApi } from "../http-api";
import { ServerSideSecretStorage } from "../secret-storage";
import { ICryptoCallbacks } from "../crypto";
import { Logger } from "../logger";
import { CryptoStore } from "../crypto/store/base";
import { CryptoStore, MigrationState } from "../crypto/store/base";
import { migrateFromLegacyCrypto, migrateRoomSettingsFromLegacyCrypto } from "./libolm_migration";
/**
@@ -152,6 +152,7 @@ async function initOlmMachine(
olmMachine.roomKeyRequestsEnabled = false;
const rustCrypto = new RustCrypto(logger, olmMachine, http, userId, deviceId, secretStorage, cryptoCallbacks);
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
rustCrypto.onRoomKeysUpdated(sessions),
);
@@ -182,5 +183,23 @@ async function initOlmMachine(
// XXX: find a less hacky way to do this.
await olmMachine.outgoingRequests();
if (legacyCryptoStore && (await legacyCryptoStore.containsData())) {
const migrationState = await legacyCryptoStore.getMigrationState();
if (migrationState < MigrationState.INITIAL_OWN_KEY_QUERY_DONE) {
logger.debug(`Performing initial key query after migration`);
// We need to do an initial keys query so that the rust stack can properly update trust of
// the user device and identity from the migrated private keys.
// If not done, there is a short period where the own device/identity trust will be undefined after migration.
try {
await rustCrypto.userHasCrossSigningKeys(userId);
} catch (e) {
// We don't want to fail the startup if this fails, but we do want to log it.
// It will be retried by the sdk.
logger.error("Failed to check for cross-signing keys after migration", e);
}
await legacyCryptoStore.setMigrationState(MigrationState.INITIAL_OWN_KEY_QUERY_DONE);
}
}
return rustCrypto;
}
+8
View File
@@ -346,10 +346,18 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
return JSON.parse(raw);
}
public async exportRoomKeysAsJson(): Promise<string> {
return await this.olmMachine.exportRoomKeys(() => true);
}
public async importRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void> {
return await this.backupManager.importRoomKeys(keys, opts);
}
public async importRoomKeysAsJson(keys: string, opts?: ImportRoomKeysOpts): Promise<void> {
return await this.backupManager.importRoomKeysAsJson(keys, opts);
}
/**
* Implementation of {@link CryptoApi.userHasCrossSigningKeys}.
*/
+3 -28
View File
@@ -22,7 +22,7 @@ import { logger } from "./logger";
import { MatrixEvent } from "./models/event";
import { EventType } from "./@types/event";
import { defer, IDeferred, removeElement } from "./utils";
import { ConnectionError, MatrixError } from "./http-api";
import { calculateRetryBackoff, MatrixError } from "./http-api";
import { ISendEventResponse } from "./@types/requests";
const DEBUG = false; // set true to enable console logging.
@@ -43,38 +43,13 @@ type ProcessFunction<T> = (event: MatrixEvent) => Promise<T>;
// eslint-disable-next-line camelcase
export class MatrixScheduler<T = ISendEventResponse> {
/**
* Retries events up to 4 times using exponential backoff. This produces wait
* times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
* failure was due to a rate limited request, the time specified in the error is
* waited before being retried.
* Default retry algorithm for the matrix scheduler. Retries events up to 4 times with exponential backoff.
* @param attempts - Number of attempts that have been made, including the one that just failed (ie. starting at 1)
* @see retryAlgorithm
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent | null, attempts: number, err: MatrixError): number {
if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) {
// client error; no amount of retrying with save you now.
return -1;
}
if (err instanceof ConnectionError) {
return -1;
}
// if event that we are trying to send is too large in any way then retrying won't help
if (err.name === "M_TOO_LARGE") {
return -1;
}
if (err.name === "M_LIMIT_EXCEEDED") {
const waitTime = err.data.retry_after_ms;
if (waitTime > 0) {
return waitTime;
}
}
if (attempts > 4) {
return -1; // give up
}
return 1000 * Math.pow(2, attempts);
return calculateRetryBackoff(err, attempts, false);
}
/**
+3 -3
View File
@@ -8,12 +8,12 @@
"noUnusedLocals": true,
"noEmit": true,
"declaration": true,
"strict": true,
"strict": true
},
"include": ["./src/**/*.ts", "./spec/**/*.ts"],
"typedocOptions": {
"entryPoints": ["src/matrix.ts"],
"excludeExternals": true,
"out": "_docs",
},
"out": "_docs"
}
}
+341 -536
View File
File diff suppressed because it is too large Load Diff