Compare commits

...

38 Commits

Author SHA1 Message Date
RiotRobot 333d6a7bd6 v40.3.0-rc.0 2026-02-11 15:06:14 +00:00
Michael Telatynski 47532de452 Switch from yarn classic to pnpm (#5184) 2026-02-11 10:35:25 +00:00
RiotRobot 87e1049dae Merge branch 'master' into develop 2026-02-10 15:17:11 +00:00
RiotRobot 4c8e38009f v40.2.0 2026-02-10 15:16:24 +00:00
Timo 6e3efef0c5 Fix empty string to room compatibility trick to only apply to m.call (#5172)
* Fix empty string to room compatibility trick to only apply to m.call

* add logging

* fix linter

* Add tests

* limit logging.
2026-02-08 11:25:34 +00:00
Olivier 'reivilibre fb590627bb Add logging on MSC4108 DELETE request (#5140)
Just noticed these requests aren't logged,
which makes debugging difficult.
This is very drive-by, done in the web editor.

Co-authored-by: R Midhun Suresh <hi@midhun.dev>
2026-02-05 10:13:44 +00:00
Michael Telatynski 24cc17c270 Merge remote-tracking branch 'origin/develop' into develop 2026-02-04 11:47:05 +00:00
Michael Telatynski 68084e8fc3 Fix vitest slow reporter crashing CI
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-04 11:46:48 +00:00
Richard van der Hoff 0c3bb1f246 Add m.invite_permission_config account data type (#5183)
For MSC4380
2026-02-04 11:00:16 +00:00
renovate[bot] 7f42b67f68 Update matrix-org (#5181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 15:11:44 +00:00
renovate[bot] 9b871ac969 Update dependency eslint-plugin-jsdoc to v62.5.0 (#5179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:39:51 +00:00
renovate[bot] 49f7972a9e Update peter-evans/repository-dispatch digest to 28959ce (#5177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:16:42 +00:00
renovate[bot] c5ae4c8c0d Update dependency typedoc-plugin-mdn-links to v5.1.1 (#5180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:02:42 +00:00
renovate[bot] 2423300acd Update npm non-major dependencies (#5178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:01:03 +00:00
renovate[bot] 6cafa175b8 Update guibranco/github-status-action-v2 digest to 9bfa877 (#5176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 13:55:10 +00:00
RiotRobot 40165942e3 v40.2.0-rc.0 2026-02-03 13:14:44 +00:00
Hugh Nimmo-Smith f301251ff5 Clean up the ValidatedAuthMetadata types (#5175)
We don't expect oidc-client-ts to provide the `device_authorization_endpoint` in the `OidcMetadata` because it isn't part of the OIDC spec.

As such, I think it makes sense to standardise on defining the metadata fields in `validate.ts` and clarify where they come from.
2026-02-02 17:27:33 +00:00
Hugh Nimmo-Smith 21cd5e98c1 Use stable /auth_metadata endpoint where homeserver supports v1.15 (#5174) 2026-02-02 15:25:48 +00:00
Andy Balaam db070dca57 Support additional_creators in upgradeRoom (MSC4289) (#5173)
* Support additional_creators in upgradeRoom (MSC4289)

Signed-off-by: Andy Balaam <andy.balaam@matrix.org>

* Remove unneeded undefined in type definition

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-02 11:24:53 +00:00
Timo 8b6ff0abcb [js sdk embedded/widget] Fix race where this.syncApi.injectRoomEvents was called before the syncApi is instantiated (#5168)
* Make sure we do not call this.syncApi.injectRoomEvents before the
syncApi is instanciated

* sonarCube remove complexity (embedded.js constructor)
2026-01-29 16:22:41 +00:00
Timo f2157f28bb [MatrixRTC] Minimal change to transition from "" to "ROOM" as the callId/slotId (#5166)
* Minimal change to transition from "" to "ROOM" as the callId/slotId

* Also transition MembershipManager tests to `"ROOM"`

* fix merge
2026-01-29 15:37:34 +00:00
Timo 739b8e1f89 Remove sending of deprecated notify event (we now use (#5167)
`m.rtc.notification`
2026-01-27 15:52:41 +00:00
Timo bc57a3f829 [MatrixRTC] Do not sent the livekit_alias in sticky events (#5165)
* Do not sent the livekit_alias in sticky events

* tests
2026-01-27 15:23:14 +00:00
RiotRobot f136f6ddf7 Merge branch 'master' into develop 2026-01-27 12:39:59 +00:00
R Midhun Suresh 82b51d0d46 Improve startup performance by using promise.all when processing rooms from sync (#5095)
* Use Promise.all when processing rooms in sync response

* Remove import
2026-01-27 08:56:26 +00:00
David Baker fb12a5a1d6 Recalculate room name on loading members (#5158)
* Recalculate room name on loading members

Because if it's a DM room, loading members might change the room name

* Swap other userA / userB constants

* Typo

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-26 12:25:41 +00:00
Hugh Nimmo-Smith 61ea5a7dfc Add OAuthGrantType enum for OAuth 2.0 API grant types (#5161)
* Add OAuthGrantType enum for OAuth 2.0 API grant types

* Resolve circular imports

* Fix complication error
2026-01-23 13:46:56 +00:00
Hugh Nimmo-Smith 4e032317fe Add support for stable OAuth2.0 aware feature from MSC3824 (#5159)
* Add support for stable OAuth2.0 aware feature from MSC3824

* Use stable name internally

* Mark DELEGATED_OIDC_COMPATIBILITY as

* Add tsdoc config for @alias JSDoc modifier
2026-01-23 09:50:12 +00:00
Robin dbb2ae5c07 Give RoomWidgetClient the ability to send and receive sticky events (#5142)
* Give RoomWidgetClient the ability to send and receive sticky events

* linter

* Fix existing tests

* Add tests for sticky event support in embedded clients

* Update sticky event widget capability identifiers

In matrix-widget-api 0.16.1 they are updated to use the new unstable prefix from MSC4407.

* Explicitly require matrix-widget-api ≥ 1.16.1

* remove TODO comment

* simplify type lint checks
This is needed for EW donwstream tests. Otherwise it will through:
Error: matrix-js-sdk/src/embedded.ts(417,21): error TS2345: Argument of
type 'string | number | boolean | string[]' is not assignable to
parameter of type 'number'.

---------

Co-authored-by: Timo K <toger5@hotmail.de>
2026-01-22 16:07:13 +00:00
Michael Telatynski c8032a214e Typescript fixes (#5157)
* Remove deprecated tsconfig baseUrl

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

* Fix imports in tests

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

* Fix duplicated map entry

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-21 17:28:10 +00:00
Timo 6e34ca6f2d [MatrixRTC] Fix delayId not resetting on leave (#5156)
* reset delay id to undefined after sucessfully sending it.

* add tests

* setAndEmitDelayId signature
dont allow implicit undefined
2026-01-21 13:53:55 +00:00
Andy Balaam 4a7a699623 Unit tests for OutgoingRequestsManager not repeating failed requests (#5154) 2026-01-21 12:03:09 +00:00
renovate[bot] 774776178e Update npm non-major dependencies (#5150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 15:08:00 +00:00
renovate[bot] ed4078528d Update dependency typedoc to v0.28.16 (#5149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 15:03:52 +00:00
renovate[bot] 7224961e9e Update actions/setup-node digest to 6044e13 (#5147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:44:56 +00:00
renovate[bot] 35ca07e8fb Update matrix-org/sonarcloud-workflow-action digest to ea0cd9d (#5148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:44:47 +00:00
renovate[bot] ccffb5df2d Update dependency eslint-plugin-jsdoc to v62 (#5153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:44:04 +00:00
renovate[bot] 0d162f66f3 Update dependency matrix-widget-api to v1.16.0 (#5152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:43:47 +00:00
46 changed files with 9138 additions and 6489 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
* @matrix-org/element-web-reviewers
/.github/workflows/** @matrix-org/element-web-team
/package.json @matrix-org/element-web-team
/yarn.lock @matrix-org/element-web-team
/pnpm-lock.yaml @matrix-org/element-web-team
/scripts/** @matrix-org/element-web-team
/src/webrtc @matrix-org/element-call-reviewers
/src/matrixrtc @matrix-org/element-call-reviewers
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
@@ -21,13 +21,14 @@ jobs:
ref: staging
fetch-depth: 0
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version-file: package.json
cache: "yarn"
cache: "pnpm"
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
id: draft-release
+5 -4
View File
@@ -33,13 +33,14 @@ jobs:
sparse-checkout: |
scripts/release
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Set up git
run: |
@@ -73,7 +74,7 @@ jobs:
fi
echo "Resetting $PACKAGE to develop branch..."
yarn add "github:matrix-org/$PACKAGE#develop"
pnpm add "github:matrix-org/$PACKAGE#develop"
git add -u
git commit -m "Reset $PACKAGE back to develop branch"
done <<< "$DEPENDENCIES"
+7 -6
View File
@@ -123,13 +123,14 @@ jobs:
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version-file: package.json
- name: Install dependencies
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Handle develop dependencies
run: |
@@ -140,14 +141,14 @@ jobs:
VERSION=${dep[1]}
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION"
yarn upgrade "$PACKAGE@$VERSION" --exact
pnpm add "$PACKAGE@$VERSION" --save-exact
git add -u
git commit -m "Keep $PACKAGE at $VERSION"
done
- name: Bump package.json version
run: |
yarn version --no-git-tag-version --new-version "${VERSION#v}"
pnpm version --no-git-tag-version "${VERSION#v}"
git add package.json
- name: Add to CHANGELOG.md
@@ -175,7 +176,7 @@ jobs:
- name: Build assets
if: steps.prepare.outputs.has-dist-script == '1'
run: DIST_VERSION="$VERSION" yarn dist
run: DIST_VERSION="$VERSION" pnpm dist
- name: Upload release assets & signatures
if: inputs.asset-path
+5 -4
View File
@@ -21,10 +21,11 @@ jobs:
with:
ref: staging
- name: 🔧 Yarn cache
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: 🔧 pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
registry-url: "https://registry.npmjs.org"
node-version-file: package.json
@@ -33,7 +34,7 @@ jobs:
run: npm install -g npm@latest
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: 🚀 Publish to npm
id: npm-publish
+11 -9
View File
@@ -50,9 +50,10 @@ jobs:
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version: "lts/*"
- name: Bump dependency
@@ -61,8 +62,8 @@ jobs:
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
yarn upgrade "$DEPENDENCY" --exact
git add package.json yarn.lock
pnpm add "$DEPENDENCY" --save-exact
git add package.json pnpm-lock.yaml
git commit -am"Upgrade dependency to $DEPENDENCY"
git push origin staging
@@ -75,17 +76,18 @@ jobs:
- name: 🧮 Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: 🔧 Yarn cache
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: 🔧 pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version-file: package.json
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: 📖 Generate docs
run: yarn gendoc
run: pnpm gendoc
- name: Upload artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
+3 -3
View File
@@ -27,7 +27,7 @@ jobs:
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
- uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
@@ -75,7 +75,7 @@ jobs:
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@9f6f057c1b30500877460c66d4ae937e53d168ee
uses: matrix-org/sonarcloud-workflow-action@ea0cd9dbd5562e79816685972bc0d03c235a900c
# workflow_run fails report against the develop commit always, we don't want that for PRs
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with:
@@ -88,7 +88,7 @@ jobs:
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
- uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
+34 -33
View File
@@ -16,16 +16,17 @@ jobs:
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
run: "pnpm install"
- name: Typecheck
run: "yarn run lint:types"
run: "pnpm run lint:types"
js_lint:
name: "ESLint"
@@ -33,16 +34,17 @@ jobs:
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
run: "pnpm install"
- name: Run Linter
run: "yarn run lint:js"
run: "pnpm run lint:js"
node_example_lint:
name: "Node.js example"
@@ -50,22 +52,17 @@ jobs:
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
run: "pnpm install"
- name: Build Types
run: "yarn build:types"
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "npm"
node-version-file: "examples/node/package.json"
# cache-dependency-path: '**/package-lock.json'
run: "pnpm build:types"
- name: Install Example Deps
run: "npm install"
@@ -85,16 +82,17 @@ jobs:
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Run Linter
run: "yarn lint:workflows"
run: "pnpm lint:workflows"
docs:
name: "JSDoc Checker"
@@ -102,16 +100,17 @@ jobs:
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
run: "pnpm install"
- name: Generate Docs
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
- name: Upload Artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
@@ -127,16 +126,17 @@ jobs:
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Run linter
run: "yarn run lint:knip"
run: "pnpm run lint:knip"
element-web:
name: Downstream tsc element-web
@@ -147,9 +147,10 @@ jobs:
with:
repository: element-hq/element-web
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version: "lts/*"
- name: Install Dependencies
@@ -159,7 +160,7 @@ jobs:
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
- name: Typecheck
run: "yarn run lint:types"
run: "pnpm run lint:types"
# Hook for branch protection to skip downstream typechecking outside of merge queues
downstream:
+6 -5
View File
@@ -24,15 +24,16 @@ jobs:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup Node
id: setupNode
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
cache: "yarn"
cache: "pnpm"
node-version: ${{ matrix.node }}
- name: Install dependencies
run: "yarn install"
run: "pnpm install"
- name: Get number of CPU cores
id: cpu-cores
@@ -40,7 +41,7 @@ jobs:
- name: Run tests
run: |
yarn test \
pnpm test \
--coverage=${{ env.ENABLE_COVERAGE }} \
--maxWorkers ${{ steps.cpu-cores.outputs.count }} \
./spec/${{ matrix.specs }}
@@ -112,7 +113,7 @@ jobs:
steps:
- name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false'
uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
+1
View File
@@ -3,6 +3,7 @@
/.npmrc
/*.log
pnpm-lock.yaml
package-lock.json
.lock-wscript
build/Release
+23
View File
@@ -1,3 +1,26 @@
Changes in [40.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.2.0) (2026-02-10)
==================================================================================================
## 🦖 Deprecations
* [MatrixRTC] Remove sending of deprecated `notify` event (we now use `m.rtc.notification`) ([#5167](https://github.com/matrix-org/matrix-js-sdk/pull/5167)). Contributed by @toger5.
## ✨ Features
* Use stable /auth\_metadata endpoint where homeserver supports v1.15 ([#5174](https://github.com/matrix-org/matrix-js-sdk/pull/5174)). Contributed by @hughns.
* Support additional\_creators in upgradeRoom (MSC4289) ([#5173](https://github.com/matrix-org/matrix-js-sdk/pull/5173)). Contributed by @andybalaam.
* [MatrixRTC] Minimal change to transition from "" to "ROOM" as the callId/slotId ([#5166](https://github.com/matrix-org/matrix-js-sdk/pull/5166)). Contributed by @toger5.
* [MatrixRTC] Do not send the `livekit_alias` in sticky events ([#5165](https://github.com/matrix-org/matrix-js-sdk/pull/5165)). Contributed by @toger5.
* Improve startup performance by using `promise.all` when processing rooms from sync ([#5095](https://github.com/matrix-org/matrix-js-sdk/pull/5095)). Contributed by @MidhunSureshR.
* Add OAuthGrantType enum for OAuth 2.0 API grant types ([#5161](https://github.com/matrix-org/matrix-js-sdk/pull/5161)). Contributed by @hughns.
* Add support for stable OAuth2.0 aware feature from MSC3824 ([#5159](https://github.com/matrix-org/matrix-js-sdk/pull/5159)). Contributed by @hughns.
* Give RoomWidgetClient the ability to send and receive sticky events ([#5142](https://github.com/matrix-org/matrix-js-sdk/pull/5142)). Contributed by @robintown.
## 🐛 Bug Fixes
* [js sdk embedded/widget] Fix race where this.syncApi.injectRoomEvents was called before the syncApi is instantiated ([#5168](https://github.com/matrix-org/matrix-js-sdk/pull/5168)). Contributed by @toger5.
* [MatrixRTC] Fix delayId not resetting on leave ([#5156](https://github.com/matrix-org/matrix-js-sdk/pull/5156)). Contributed by @toger5.
Changes in [40.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.1.0) (2026-01-27)
==================================================================================================
## 🦖 Deprecations
+8 -8
View File
@@ -41,10 +41,10 @@ endpoints from before Matrix 1.1, for example.
> Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.
Using `pnpm` instead of `npm` is recommended. Please see the pnpm [install
guide](https://pnpm.io/installation#using-corepack) if you do not have it already.
`yarn add matrix-js-sdk`
`pnpm add matrix-js-sdk`
```javascript
import * as sdk from "matrix-js-sdk";
@@ -310,7 +310,7 @@ This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. Yo
host the API reference from the source files like this:
```
$ yarn gendoc
$ pnpm gendoc
$ cd docs
$ python -m http.server 8005
```
@@ -453,7 +453,7 @@ want to use this SDK, skip this section._
First, you need to pull in the right build tools:
```
$ yarn install
$ pnpm install
```
## Building
@@ -461,17 +461,17 @@ First, you need to pull in the right build tools:
To build a browser version from scratch when developing:
```
$ yarn build
$ pnpm build
```
To run tests:
```
$ yarn test
$ pnpm test
```
To run linting:
```
$ yarn lint
$ pnpm lint
```
+1 -1
View File
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks
cd "$tmpdir"
yarn lint
pnpm lint
+21 -10
View File
@@ -1,19 +1,19 @@
{
"name": "matrix-js-sdk",
"version": "40.1.0",
"version": "40.3.0-rc.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=22.0.0"
},
"scripts": {
"prepare": "yarn build",
"prepare": "pnpm build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"clean": "rimraf lib",
"build": "yarn clean && yarn build:compile && yarn build:types",
"build": "pnpm clean && pnpm build:compile && pnpm build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js && yarn lint:workflows",
"lint": "pnpm lint:types && pnpm lint:js && pnpm lint:workflows",
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
"lint:types": "tsc --noEmit",
@@ -21,7 +21,7 @@
"lint:knip": "knip",
"test": "vitest",
"test:watch": "vitest --watch",
"coverage": "yarn test --coverage"
"coverage": "pnpm test --coverage"
},
"repository": {
"type": "git",
@@ -56,7 +56,7 @@
"jwt-decode": "^4.0.0",
"loglevel": "^1.9.2",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.14.0",
"matrix-widget-api": "^1.16.1",
"oidc-client-ts": "^3.0.1",
"p-retry": "7",
"sdp-transform": "^3.0.0",
@@ -84,7 +84,7 @@
"@stylistic/eslint-plugin": "^5.0.0",
"@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7",
"@types/node": "18",
"@types/node": "22",
"@types/sdp-transform": "^2.4.5",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
@@ -98,7 +98,7 @@
"eslint-config-prettier": "^10.0.0",
"eslint-import-resolver-typescript": "^4.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^61.0.0",
"eslint-plugin-jsdoc": "^62.0.0",
"eslint-plugin-matrix-org": "^3.0.0",
"eslint-plugin-n": "^14.0.0",
"eslint-plugin-tsdoc": "^0.5.0",
@@ -110,7 +110,7 @@
"knip": "^5.0.0",
"lint-staged": "^16.0.0",
"matrix-mock-request": "^2.5.0",
"prettier": "3.7.4",
"prettier": "3.8.1",
"rimraf": "^6.0.0",
"typedoc": "^0.28.1",
"typedoc-plugin-coverage": "^4.0.0",
@@ -122,5 +122,16 @@
},
"resolutions": {
"expect": "30.2.0"
}
},
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"eslint": "8"
}
},
"allowedDeprecatedVersions": {
"eslint": "8"
}
},
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264"
}
+7956
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
nodeLinker: hoisted
+171 -1
View File
@@ -32,7 +32,14 @@ import {
type IRoomEvent,
} from "matrix-widget-api";
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import {
createRoomWidgetClient,
EventType,
type IEvent,
MatrixError,
MsgType,
UpdateDelayedEventAction,
} from "../../src/matrix";
import { MatrixClient, ClientEvent, type ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
@@ -42,6 +49,7 @@ import { sleep } from "../../src/utils";
import { SlidingSync } from "../../src/sliding-sync";
import { logger } from "../../src/logger";
import { flushPromises } from "../test-utils/flushPromises";
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../../src/models/room-sticky-events";
const testOIDCToken = {
access_token: "12345678",
@@ -49,6 +57,7 @@ const testOIDCToken = {
matrix_server_name: "homeserver.oabc",
token_type: "Bearer",
};
class MockWidgetApi extends EventEmitter {
public start = vi.fn().mockResolvedValue(undefined);
public getClientVersions = vi.fn();
@@ -167,6 +176,9 @@ describe("RoomWidgetClient", () => {
"org.matrix.rageshake_request",
{ request_id: 123 },
"!1:example.org",
undefined,
undefined,
undefined,
);
});
@@ -422,6 +434,7 @@ describe("RoomWidgetClient", () => {
"!1:example.org",
2000,
undefined,
undefined,
);
});
@@ -442,6 +455,7 @@ describe("RoomWidgetClient", () => {
"!1:example.org",
undefined,
parentDelayId,
undefined,
);
});
@@ -855,6 +869,162 @@ describe("RoomWidgetClient", () => {
});
});
describe("sticky events", () => {
describe("when supported", () => {
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4354"),
);
beforeAll(() => {
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
});
afterAll(() => {
doesServerSupportUnstableFeatureMock.mockReset();
});
it("requests capabilities when set", async () => {
await makeClient({ sendSticky: true, receiveSticky: true });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407ReceiveStickyEvent);
});
it("does not request capabilities when unset", async () => {
await makeClient({});
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(
MatrixCapabilities.MSC4407ReceiveStickyEvent,
);
});
it("sends", async () => {
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith(EventType.RTCMembership);
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
await client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
msc4354_sticky_key: "test",
});
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
EventType.RTCMembership,
{ msc4354_sticky_key: "test" },
"!1:example.org",
undefined,
undefined,
2000,
);
});
it("receives (adds, updates, then removes when redacted)", async () => {
await makeClient({ receiveEvent: [EventType.RTCMembership, EventType.RoomRedaction] });
const room = client.getRoom("!1:example.org")!;
function expectStickyEvents(events: IEvent[]) {
expect([...room._unstable_getStickyEvents()].map((e) => e.getEffectiveEvent())).toEqual(events);
}
async function sendAndExpectStickyUpdate(
eventToSend: IEvent,
added: IEvent[],
updated: { current: IEvent; previous: IEvent }[],
removed: IEvent[],
) {
const emittedStickyUpdate = new Promise<
Parameters<RoomStickyEventsMap[RoomStickyEventsEvent.Update]>
>((resolve) => room.once(RoomStickyEventsEvent.Update, (...args) => resolve(args)));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
detail: { data: eventToSend },
}),
);
const [addedReceived, updatedReceived, removedReceived] = await emittedStickyUpdate;
expect(addedReceived.map((e) => e.getEffectiveEvent())).toEqual(added);
expect(
updatedReceived.map(({ current, previous }) => ({
current: current.getEffectiveEvent(),
previous: previous.getEffectiveEvent(),
})),
).toEqual(updated);
expect(removedReceived.map((e) => e.getEffectiveEvent())).toEqual(removed);
}
// First, add a new sticky event to the map. The client should emit.
const event1 = new MatrixEvent({
type: EventType.RTCMembership,
event_id: "$pduhfiidph",
room_id: "!1:example.org",
sender: "@alice:example.org",
msc4354_sticky: { duration_ms: 1200000 },
content: { msc4354_sticky_key: "test" },
}).getEffectiveEvent();
await sendAndExpectStickyUpdate(event1, [event1], [], []);
// It should remain cached in the sticky map
expectStickyEvents([event1]);
// Next, update the same key in the sticky map
const event2 = new MatrixEvent({
type: EventType.RTCMembership,
event_id: "$zshgyutptfh",
room_id: "!1:example.org",
sender: "@alice:example.org",
msc4354_sticky: { duration_ms: 1200000 },
content: { msc4354_sticky_key: "test" },
}).getEffectiveEvent();
await sendAndExpectStickyUpdate(event2, [], [{ current: event2, previous: event1 }], []);
expectStickyEvents([event2]);
// Next, redact the second event. Because it has the first as a predecessor, the map should revert to
// the first event.
const redaction1 = new MatrixEvent({
type: EventType.RoomRedaction,
event_id: "$cimoexnvz",
room_id: "!1:example.org",
sender: "@alice:example.org",
redacts: event2.event_id,
content: { redacts: event2.event_id },
}).getEffectiveEvent();
await sendAndExpectStickyUpdate(redaction1, [], [{ current: event1, previous: event2 }], []);
expectStickyEvents([event1]);
// Finally, redact the first event. Now everything should be gone from the map.
const redaction2 = new MatrixEvent({
type: EventType.RoomRedaction,
event_id: "$drgzmenlh",
room_id: "!1:example.org",
sender: "@alice:example.org",
redacts: event1.event_id,
content: { redacts: event1.event_id },
}).getEffectiveEvent();
await sendAndExpectStickyUpdate(redaction2, [], [], [event1]);
expectStickyEvents([]);
});
});
describe("when unsupported", () => {
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
beforeAll(() => {
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
});
afterAll(() => {
doesServerSupportUnstableFeatureMock.mockReset();
});
it("fails to send", async () => {
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
await expect(
client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
msc4354_sticky_key: "test",
}),
).rejects.toThrow("Server does not support");
});
});
});
describe("to-device messages", () => {
const unencryptedContentMap = new Map([
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
+3
View File
@@ -51,18 +51,21 @@ describe("SSO login URL", function () {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
const url = new URL(urlString);
expect(url.searchParams.has("org.matrix.msc3824.action")).toBe(false);
expect(url.searchParams.has("action")).toBe(false);
});
it("register", function () {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
const url = new URL(urlString);
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("register");
expect(url.searchParams.get("action")).toEqual("register");
});
it("login", function () {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
const url = new URL(urlString);
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("login");
expect(url.searchParams.get("action")).toEqual("login");
});
});
});
+22 -1
View File
@@ -88,7 +88,7 @@ import { mockOpenIdConfiguration } from "../test-utils/oidc.ts";
import { type CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { SyncResponder } from "../test-utils/SyncResponder.ts";
import { mockInitialApiRequests } from "../test-utils/mockEndpoints.ts";
import { type Transport } from "src/matrixrtc/index.ts";
import { type Transport } from "../../src/matrixrtc/index.ts";
vi.useFakeTimers();
@@ -3877,6 +3877,27 @@ describe("MatrixClient", function () {
makeClient();
});
it("should use stable prefix", async () => {
const metadata = mockOpenIdConfiguration();
client.getVersions = vi.fn().mockResolvedValue({
versions: ["v1.15"],
});
httpLookups = [
{
method: "GET",
path: `/auth_metadata`,
data: metadata,
prefix: "/_matrix/client/v1",
},
];
await expect(client.getAuthMetadata()).resolves.toEqual({
...metadata,
signingKeys: [],
});
expect(httpLookups.length).toEqual(0);
});
it("should use unstable prefix", async () => {
const metadata = mockOpenIdConfiguration();
httpLookups = [
+29 -2
View File
@@ -158,8 +158,35 @@ describe("CallMembership", () => {
expect(membership.eventId).toBe("$eventid");
});
it("returns correct slot_id", () => {
expect(membership.slotId).toBe("m.call#");
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
// slot_id is application and call_id dependent. So we create
// a membership for each possible combination
// non call application (should not alter call_id even with empty string)
const nonCallMembership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
application: "m.not.a.call",
call_id: "",
});
// non "" call id should not be altered
const callMembershipCustomId = createCallMembership(makeMockEvent(), {
...membershipTemplate,
call_id: "customCallId",
});
// for membership (application = m.call and call_id = "") we expect "" -> ROOM
// for legacy events we expect the room to be added automagically
// See INFO_SLOT_ID_LEGACY_CASE comments
expect(membership.slotId).toBe("m.call#ROOM");
expect(membership.slotDescription).toStrictEqual({ id: "ROOM", application: "m.call" });
expect(nonCallMembership.slotId).toBe("m.not.a.call#");
expect(nonCallMembership.slotDescription).toStrictEqual({ id: "", application: "m.not.a.call" });
expect(callMembershipCustomId.slotId).toBe("m.call#customCallId");
expect(callMembershipCustomId.slotDescription).toStrictEqual({
id: "customCallId",
application: "m.call",
});
});
it("returns correct deviceId", () => {
expect(membership.deviceId).toBe("AAAAAAA");
+32 -68
View File
@@ -49,7 +49,7 @@ const mockFocus = { type: "mock" };
const textEncoder = new TextEncoder();
const callSession = { id: "", application: "m.call" };
const callSession = { id: "ROOM", application: "m.call" };
describe("MatrixRTCSession", () => {
let client: MatrixClient;
@@ -134,12 +134,12 @@ describe("MatrixRTCSession", () => {
);
await flushPromises();
expect(sess?.memberships.length).toEqual(1);
expect(sess?.memberships[0].slotDescription.id).toEqual("");
expect(sess?.memberships[0].slotDescription.id).toEqual("ROOM");
expect(sess?.memberships[0].scope).toEqual("m.room");
expect(sess?.memberships[0].application).toEqual("m.call");
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
expect(sess?.memberships[0].isExpired()).toEqual(false);
expect(sess?.slotDescription.id).toEqual("");
expect(sess?.slotDescription.id).toEqual("ROOM");
});
it("ignores memberships where application is not m.call", () => {
@@ -381,12 +381,12 @@ describe("MatrixRTCSession", () => {
});
await flushPromises();
expect(sess?.memberships.length).toEqual(1);
expect(sess?.memberships[0].slotDescription.id).toEqual("");
expect(sess?.memberships[0].slotDescription.id).toEqual("ROOM");
expect(sess?.memberships[0].scope).toEqual("m.room");
expect(sess?.memberships[0].application).toEqual("m.call");
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
expect(sess?.memberships[0].isExpired()).toEqual(false);
expect(sess?.slotDescription.id).toEqual("");
expect(sess?.slotDescription.id).toEqual("ROOM");
});
it("combines sticky and membership events when both exist", async () => {
// Create a room with identical member state and sticky state for the same user.
@@ -415,7 +415,7 @@ describe("MatrixRTCSession", () => {
const memberships = sess.memberships;
expect(memberships.length).toEqual(2);
expect(memberships[0].sender).toEqual(stickyUserId);
expect(memberships[0].slotDescription.id).toEqual("");
expect(memberships[0].slotDescription.id).toEqual("ROOM");
expect(memberships[0].scope).toEqual("m.room");
expect(memberships[0].application).toEqual("m.call");
expect(memberships[0].deviceId).toEqual("AAAAAAA");
@@ -424,7 +424,7 @@ describe("MatrixRTCSession", () => {
// Then state
expect(memberships[1].sender).toEqual(membershipTemplate.user_id);
expect(sess?.slotDescription.id).toEqual("");
expect(sess?.slotDescription.id).toEqual("ROOM");
});
it("handles an incoming sticky event to an existing session", async () => {
const mockRoom = makeMockRoom([membershipTemplate]);
@@ -620,9 +620,7 @@ describe("MatrixRTCSession", () => {
it("sends a notification when starting a call and emit DidSendCallNotification", async () => {
// Simulate a join, including the update to the room state
// Ensure sendEvent returns event IDs so the DidSendCallNotification payload includes them
sendEventMock
.mockResolvedValueOnce({ event_id: "legacy-evt" })
.mockResolvedValueOnce({ event_id: "new-evt" });
sendEventMock.mockResolvedValueOnce({ event_id: "new-evt" });
const didSendEventFn = vi.fn();
sess!.once(MatrixRTCSessionEvent.DidSendCallNotification, didSendEventFn);
// Create an additional listener to create a promise that resolves after the emission.
@@ -647,43 +645,25 @@ describe("MatrixRTCSession", () => {
"sender_ts": expect.any(Number),
});
// Check if deprecated notify event is also sent.
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.CallNotify, {
"application": "m.call",
"m.mentions": { user_ids: [], room: true },
"notify_type": "ring",
"call_id": "",
});
await didSendNotification;
// And ensure we emitted the DidSendCallNotification event with both payloads
expect(didSendEventFn).toHaveBeenCalledWith(
{
"event_id": "new-evt",
"lifetime": 30000,
"m.mentions": { room: true, user_ids: [] },
"m.relates_to": {
event_id: expect.any(String),
rel_type: "m.reference",
},
"notification_type": "ring",
"sender_ts": expect.any(Number),
expect(didSendEventFn).toHaveBeenCalledWith({
"event_id": "new-evt",
"lifetime": 30000,
"m.mentions": { room: true, user_ids: [] },
"m.relates_to": {
event_id: expect.any(String),
rel_type: "m.reference",
},
{
"application": "m.call",
"call_id": "",
"event_id": "legacy-evt",
"m.mentions": { room: true, user_ids: [] },
"notify_type": "ring",
},
);
"notification_type": "ring",
"sender_ts": expect.any(Number),
});
});
it("sends a notification with a intent when starting a call and emits DidSendCallNotification", async () => {
// Simulate a join, including the update to the room state
// Ensure sendEvent returns event IDs so the DidSendCallNotification payload includes them
sendEventMock
.mockResolvedValueOnce({ event_id: "legacy-evt" })
.mockResolvedValueOnce({ event_id: "new-evt" });
sendEventMock.mockResolvedValueOnce({ event_id: "new-evt" });
const didSendEventFn = vi.fn();
sess!.once(MatrixRTCSessionEvent.DidSendCallNotification, didSendEventFn);
// Create an additional listener to create a promise that resolves after the emission.
@@ -707,7 +687,7 @@ describe("MatrixRTCSession", () => {
]);
await sess!._onRTCSessionMemberUpdate();
const ownMembershipId = sess?.memberships[0].eventId;
const ownMembershipEventId = sess?.memberships[0].eventId;
expect(sess!.getConsensusCallIntent()).toEqual("audio");
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.RTCNotification, {
@@ -715,43 +695,27 @@ describe("MatrixRTCSession", () => {
"notification_type": "ring",
"m.call.intent": "audio",
"m.relates_to": {
event_id: ownMembershipId,
event_id: ownMembershipEventId,
rel_type: "m.reference",
},
"lifetime": 30000,
"sender_ts": expect.any(Number),
});
// Check if deprecated notify event is also sent.
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.CallNotify, {
"application": "m.call",
"m.mentions": { user_ids: [], room: true },
"notify_type": "ring",
"call_id": "",
});
await didSendNotification;
// And ensure we emitted the DidSendCallNotification event with both payloads
expect(didSendEventFn).toHaveBeenCalledWith(
{
"event_id": "new-evt",
"lifetime": 30000,
"m.mentions": { room: true, user_ids: [] },
"m.relates_to": {
event_id: expect.any(String),
rel_type: "m.reference",
},
"notification_type": "ring",
"m.call.intent": "audio",
"sender_ts": expect.any(Number),
expect(didSendEventFn).toHaveBeenCalledWith({
"event_id": "new-evt",
"lifetime": 30000,
"m.mentions": { room: true, user_ids: [] },
"m.relates_to": {
event_id: expect.any(String),
rel_type: "m.reference",
},
{
"application": "m.call",
"call_id": "",
"event_id": "legacy-evt",
"m.mentions": { room: true, user_ids: [] },
"notify_type": "ring",
},
);
"notification_type": "ring",
"m.call.intent": "audio",
"sender_ts": expect.any(Number),
});
});
it("doesn't send a notification when joining an existing call", async () => {
+67 -4
View File
@@ -71,7 +71,7 @@ function createAsyncHandle<T>(method: MockedFunction<(...args: any[]) => any>) {
return { reject, resolve };
}
const callSession = { id: "", application: "m.call" };
const callSession = { id: "ROOM", application: "m.call" };
describe("MembershipManager", () => {
let client: MockClient;
@@ -139,6 +139,7 @@ describe("MembershipManager", () => {
"org.matrix.msc3401.call.member",
{
application: "m.call",
// This tests INFO_SLOT_ID_LEGACY_CASE because it is using callSession = { id: "ROOM", application: "m.call" }
call_id: "",
device_id: "AAAAAAA",
expires: 14400000,
@@ -147,6 +148,7 @@ describe("MembershipManager", () => {
focus_active: focusActive,
scope: "m.room",
},
// This tests INFO_SLOT_ID_LEGACY_CASE because it is using callSession = { id: "ROOM", application: "m.call" }
"_@alice:example.org_AAAAAAA_m.call",
);
restartScheduledDelayedEventHandle.resolve?.();
@@ -160,6 +162,45 @@ describe("MembershipManager", () => {
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
});
it("sends correct call_id and state key when using non empty string. Not using empty string -> ROOM hack. See: INFO_SLOT_ID_LEGACY_CASE", async () => {
// Spys/Mocks
const customCallSession = { id: "custom", application: "m.call" };
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
client._unstable_restartScheduledDelayedEvent,
);
// Test
const memberManager = new MembershipManager(undefined, room, client, customCallSession);
memberManager.join([focus], undefined);
// expects
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
"org.matrix.msc3401.call.member",
{
application: "m.call",
call_id: "custom",
device_id: "AAAAAAA",
expires: 14400000,
foci_preferred: [focus],
membershipID: "@alice:example.org:AAAAAAA",
focus_active: focusActive,
scope: "m.room",
},
"_@alice:example.org_AAAAAAA_m.callcustom",
);
restartScheduledDelayedEventHandle.resolve?.();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
room.roomId,
{ delay: 8000 },
"org.matrix.msc3401.call.member",
{},
"_@alice:example.org_AAAAAAA_m.callcustom",
);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
});
it("reschedules delayed leave event if sending state cancels it", async () => {
const memberManager = new MembershipManager(undefined, room, client, callSession);
const waitForSendState = waitForMockCall(client.sendStateEvent);
@@ -416,8 +457,9 @@ describe("MembershipManager", () => {
await manager.leave();
expect(client._unstable_sendScheduledDelayedEvent).toHaveBeenLastCalledWith("id");
expect(client.sendStateEvent).toHaveBeenCalled();
expect(manager.delayId).toBe(undefined);
});
it("send leave event when leave is called and resolving delayed leave fails", async () => {
it("send leave event when leave is called and resolving delayed leave fails unknown error", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus]);
await vi.advanceTimersByTimeAsync(1);
@@ -431,6 +473,27 @@ describe("MembershipManager", () => {
{},
"_@alice:example.org_AAAAAAA_m.call",
);
// If there is a unknown error, we do not reset the delayId
// The delayed event might still be around and we track it.
expect(manager.delayId).not.toBe(undefined);
});
it("send leave event when leave is called and resolving delayed leave fails not found error", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus]);
await vi.advanceTimersByTimeAsync(1);
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockRejectedValue(
new MatrixError({ errcode: "M_NOT_FOUND" }, 404),
);
await manager.leave();
// We send a normal leave event since we failed using sendScheduledDelayedEvent.
expect(client.sendStateEvent).toHaveBeenLastCalledWith(
room.roomId,
"org.matrix.msc3401.call.member",
{},
"_@alice:example.org_AAAAAAA_m.call",
);
expect(manager.delayId).toBe(undefined);
});
it("does nothing if not joined", async () => {
const manager = new MembershipManager({}, room, client, callSession);
@@ -947,8 +1010,8 @@ describe("MembershipManager", () => {
id: "@alice:example.org:AAAAAAA_m.call",
device_id: "AAAAAAA",
},
slot_id: "m.call#",
rtc_transports: [focus],
slot_id: "m.call#ROOM",
rtc_transports: [{ type: focus.type, livekit_service_url: focus.livekit_service_url }],
versions: [],
msc4354_sticky_key: "@alice:example.org:AAAAAAA_m.call",
},
+1 -1
View File
@@ -20,7 +20,7 @@ import { type Mocked, type MockedObject } from "vitest";
import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent } from "../../../src";
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc";
import { secureRandomString } from "../../../src/randomstring";
import { type CallMembershipIdentityParts } from "src/matrixrtc/EncryptionManager";
import { type CallMembershipIdentityParts } from "../../../src/matrixrtc/EncryptionManager";
import { logger } from "../../../src/logger.ts";
export type MembershipData = (SessionMembershipData | {}) & { user_id: string };
+57
View File
@@ -0,0 +1,57 @@
/*
Copyright 2026 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "../../src/client";
describe("Room upgrades", function () {
it("Sends an HTTP request upgrading the room", () => {
// Given a client with a fake authedRequest method
const { client, authedRequest } = createClient();
// When we upgrade the room to version 12
client.upgradeRoom("!r1", "12");
// Then we make an HTTP request to the correct endpoint, with the
// version provided in the JSON.
expect(authedRequest).toHaveBeenCalledWith("POST", "/rooms/!r1/upgrade", undefined, { new_version: "12" });
});
it("Includes additional_creators if provided", () => {
// Given a client with a fake authedRequest method
const { client, authedRequest } = createClient();
// When we upgrade the room to version 13 and supply additionalCreators
client.upgradeRoom("!r1", "13", ["@u:s.co", "@v:a.b"]);
// Then we make an HTTP request to the correct endpoint, with the
// version and additional creators provided.
expect(authedRequest).toHaveBeenCalledWith("POST", "/rooms/!r1/upgrade", undefined, {
new_version: "13",
additional_creators: ["@u:s.co", "@v:a.b"],
});
});
});
///
function createClient(): { client: MatrixClient; authedRequest: any } {
const authedRequest = vi.fn();
const client = new MatrixClient({
baseUrl: "https://my.home.server",
userId: "@u:s.co",
});
client.http.authedRequest = authedRequest;
return { client, authedRequest };
}
@@ -236,4 +236,105 @@ describe("OutgoingRequestsManager", () => {
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(1);
});
});
describe("Repeated processing of outgoing requests", () => {
it("Processes any requests still in the queue after processing", async () => {
// Given that the first time we call outgoingRequests we get back
// requests 1 and 2, but the second time, we get request 3
const [request1, request2, request3] = setupOutgoingTwoRequestsThenOne();
// (And requests finish immediately)
processor.makeOutgoingRequest.mockImplementation(async () => {
return;
});
// When we ask a manager to process the requests
await manager.doProcessOutgoingRequests();
// Then all three are processed because we re-call outgoingRequests
// to check for more
await vi.waitFor(() => expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(3));
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(3);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request3);
});
it("Does reprocess if any request succeeded, even if some failed", async () => {
// Given that the first time we call outgoingRequests we get back
// requests 1 and 2, but the second time, we get request 3
const [request1, request2, request3] = setupOutgoingTwoRequestsThenOne();
// And the first request fails, but subsequent ones pass
processor.makeOutgoingRequest
.mockImplementationOnce(async () => {
throw new Error("This request failed!");
})
.mockImplementation(async () => {
return;
});
// When we ask a manager to process the requests
await manager.doProcessOutgoingRequests();
// Then all three are processed because we re-call outgoingRequests
// to check for more, even though one failed
await vi.waitFor(() => expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(3));
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(3);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request3);
});
it("Does not reprocess if all requests failed", async () => {
// Given that the first time we call outgoingRequests we get back
// requests 1 and 2, but the second time, we get request 3
const [request1, request2, request3] = setupOutgoingTwoRequestsThenOne();
// And the first two requests fail, but subsequent ones pass
processor.makeOutgoingRequest
.mockImplementationOnce(async () => {
throw new Error("Request 1 failed!");
})
.mockImplementationOnce(async () => {
throw new Error("Request 2 failed!");
})
.mockImplementation(async () => {
return;
});
// When we ask a manager to process the requests
await manager.doProcessOutgoingRequests();
// Then only the first two requests are processed, because since
// they both failed we stop retrying
await vi.waitFor(() => expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1));
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(2);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
expect(processor.makeOutgoingRequest).not.toHaveBeenCalledWith(request3);
});
/// Mock calls to olmMachine.outgoingRequests. The first time it is
// called, return two requests and the second time return a third one.
//
// Returns the three returned requests.
function setupOutgoingTwoRequestsThenOne(): [OutgoingRequest, OutgoingRequest, OutgoingRequest] {
const request1 = new RustSdkCryptoJs.KeysQueryRequest("1", "{}");
const request2 = new RustSdkCryptoJs.KeysUploadRequest("2", "{}");
const request3 = new RustSdkCryptoJs.KeysUploadRequest("3", "{}");
// Given that the first time we call outgoingRequests we get back
// requests 1 and 2, but the second time, we get request 3
olmMachine.outgoingRequests
.mockImplementationOnce(async (): Promise<OutgoingRequest[]> => {
return [request1, request2];
})
.mockImplementationOnce(async (): Promise<OutgoingRequest[]> => {
return [request3];
});
return [request1, request2, request3];
}
});
});
+10 -4
View File
@@ -48,11 +48,17 @@ export interface IPasswordFlow extends ILoginFlow {
type: "m.login.password";
}
export const DELEGATED_OIDC_COMPATIBILITY = new UnstableValue(
"delegated_oidc_compatibility",
export const OAUTH_AWARE_PREFERRED_FLOW_FIELD = new UnstableValue(
"oauth_aware_preferred",
"org.matrix.msc3824.delegated_oidc_compatibility",
);
/**
* @alias
* @deprecated use `OAUTH_AWARE_PREFERRED_FLOW_FIELD` instead.
*/
export const DELEGATED_OIDC_COMPATIBILITY = OAUTH_AWARE_PREFERRED_FLOW_FIELD;
/**
* Representation of SSO flow as per https://spec.matrix.org/v1.3/client-server-api/#client-login-via-sso
*/
@@ -60,8 +66,8 @@ export interface ISSOFlow extends ILoginFlow {
type: "m.login.sso" | "m.login.cas";
// eslint-disable-next-line camelcase
identity_providers?: IIdentityProvider[];
[DELEGATED_OIDC_COMPATIBILITY.name]?: boolean;
[DELEGATED_OIDC_COMPATIBILITY.altName]?: boolean;
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.name]?: boolean;
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.altName]?: boolean;
}
export enum IdentityProviderBrand {
+3
View File
@@ -138,6 +138,7 @@ export enum EventType {
PushRules = "m.push_rules",
Direct = "m.direct",
IgnoredUserList = "m.ignored_user_list",
InvitePermissionConfig = "m.invite_permission_config", // MSC4380
// to_device events
RoomKey = "m.room_key",
@@ -404,6 +405,8 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents {
// Invites-ignorer events
[POLICIES_ACCOUNT_EVENT_TYPE.name]: { [key: string]: any };
[POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any };
[EventType.InvitePermissionConfig]: { default_action?: string };
}
/**
-24
View File
@@ -1,24 +0,0 @@
/*
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 "oidc-client-ts";
declare module "oidc-client-ts" {
interface OidcMetadata {
// Add the missing device_authorization_endpoint field to the OidcMetadata interface
device_authorization_endpoint?: string;
}
}
+19 -8
View File
@@ -6689,6 +6689,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const params = {
redirectUrl,
[SSO_ACTION_PARAM.stable!]: action,
[SSO_ACTION_PARAM.unstable!]: action,
};
@@ -6936,13 +6937,23 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Upgrades a room to a new protocol version
* @param newVersion - The target version to upgrade to
* @param additionalCreators - an optional list of user IDs of users who
* should have the same permissions as the user performing the
* upgrade
* @returns Promise which resolves: Object with key 'replacement_room'
* @returns Rejects: with an error response.
*/
public upgradeRoom(roomId: string, newVersion: string): Promise<{ replacement_room: string }> {
public upgradeRoom(
roomId: string,
newVersion: string,
additionalCreators?: string[],
): Promise<{ replacement_room: string }> {
// eslint-disable-line camelcase
const path = utils.encodeUri("/rooms/$roomId/upgrade", { $roomId: roomId });
return this.http.authedRequest(Method.Post, path, undefined, { new_version: newVersion });
return this.http.authedRequest(Method.Post, path, undefined, {
new_version: newVersion,
additional_creators: additionalCreators,
});
}
/**
@@ -8826,21 +8837,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
/**
* Discover and validate delegated auth configuration
* - delegated auth issuer openid-configuration is reachable
* - delegated auth issuer openid-configuration is configured correctly for us
* Discover and validate the auth metadata for the OAuth 2.0 API.
*
* Fetches /auth_metadata falling back to legacy implementation using /auth_issuer followed by
* https://oidc-issuer.example.com/.well-known/openid-configuration and other files linked therein.
* When successful, validated metadata is returned
* When successful, validated metadata is returned.
*
* @returns validated authentication metadata and optionally signing keys
* @throws when delegated auth config is invalid or unreachable
* @experimental - part of MSC2965
*/
public async getAuthMetadata(): Promise<OidcClientConfig> {
let authMetadata: unknown | undefined;
try {
const useStable = await this.isVersionSupported("v1.15");
authMetadata = await this.http.request<unknown>(Method.Get, "/auth_metadata", undefined, undefined, {
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
prefix: useStable ? ClientPrefix.V1 : ClientPrefix.Unstable + "/org.matrix.msc2965",
});
} catch (e) {
if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") {
+110 -54
View File
@@ -38,6 +38,7 @@ import {
type SendDelayedEventRequestOpts,
type SendDelayedEventResponse,
UpdateDelayedEventAction,
isSendDelayedEventRequestOpts,
} from "./@types/requests.ts";
import { EventType, type StateEvents } from "./@types/event.ts";
import { logger } from "./logger.ts";
@@ -56,7 +57,7 @@ import { ConnectionError, MatrixError } from "./http-api/errors.ts";
import { User } from "./models/user.ts";
import { type Room } from "./models/room.ts";
import { type ToDeviceBatch, type ToDevicePayload } from "./models/ToDeviceMessage.ts";
import { MapWithDefault, recursiveMapToObject } from "./utils.ts";
import { MapWithDefault, type QueryDict, recursiveMapToObject } from "./utils.ts";
import { type EmptyObject, TypedEventEmitter, UnsupportedDelayedEventsEndpointError } from "./matrix.ts";
interface IStateEventRequest {
@@ -122,6 +123,20 @@ export interface ICapabilities {
* @defaultValue false
*/
updateDelayedEvents?: boolean;
/**
* Whether this client needs to be able to send sticky events.
* @experimental Part of MSC4354 & MSC4407
* @defaultValue false
*/
sendSticky?: boolean;
/**
* Whether this client needs to be able to receive sticky events.
* @experimental Part of MSC4354 & MSC4407
* @defaultValue false
*/
receiveSticky?: boolean;
}
export enum RoomWidgetClientEvent {
@@ -197,54 +212,7 @@ export class RoomWidgetClient extends MatrixClient {
)
: Promise.resolve();
// Request capabilities for the functionality this client needs to support
if (
capabilities.sendEvent?.length ||
capabilities.receiveEvent?.length ||
capabilities.sendMessage === true ||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
capabilities.receiveMessage === true ||
(Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length) ||
capabilities.sendState?.length ||
capabilities.receiveState?.length
) {
widgetApi.requestCapabilityForRoomTimeline(roomId);
}
capabilities.sendEvent?.forEach((eventType) => widgetApi.requestCapabilityToSendEvent(eventType));
capabilities.receiveEvent?.forEach((eventType) => widgetApi.requestCapabilityToReceiveEvent(eventType));
if (capabilities.sendMessage === true) {
widgetApi.requestCapabilityToSendMessage();
} else if (Array.isArray(capabilities.sendMessage)) {
capabilities.sendMessage.forEach((msgType) => widgetApi.requestCapabilityToSendMessage(msgType));
}
if (capabilities.receiveMessage === true) {
widgetApi.requestCapabilityToReceiveMessage();
} else if (Array.isArray(capabilities.receiveMessage)) {
capabilities.receiveMessage.forEach((msgType) => widgetApi.requestCapabilityToReceiveMessage(msgType));
}
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
widgetApi.requestCapabilityToSendState(eventType, stateKey),
);
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
);
capabilities.sendToDevice?.forEach((eventType) => widgetApi.requestCapabilityToSendToDevice(eventType));
capabilities.receiveToDevice?.forEach((eventType) => widgetApi.requestCapabilityToReceiveToDevice(eventType));
if (
capabilities.sendDelayedEvents &&
(capabilities.sendEvent?.length ||
capabilities.sendMessage === true ||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
capabilities.sendState?.length)
) {
widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
}
if (capabilities.updateDelayedEvents) {
widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
}
if (capabilities.turnServers) {
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
}
this.requestInitialCapabilities(capabilities, roomId);
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
@@ -260,10 +228,70 @@ export class RoomWidgetClient extends MatrixClient {
if (sendContentLoaded) widgetApi.sendContentLoaded();
}
private requestInitialCapabilities(capabilities: ICapabilities, roomId: string): void {
// Request capabilities for the functionality this client needs to support
if (
capabilities.sendEvent?.length ||
capabilities.receiveEvent?.length ||
capabilities.sendMessage === true ||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
capabilities.receiveMessage === true ||
(Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length) ||
capabilities.sendState?.length ||
capabilities.receiveState?.length
) {
this.widgetApi.requestCapabilityForRoomTimeline(roomId);
}
capabilities.sendEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToSendEvent(eventType));
capabilities.receiveEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToReceiveEvent(eventType));
if (capabilities.sendMessage === true) {
this.widgetApi.requestCapabilityToSendMessage();
} else if (Array.isArray(capabilities.sendMessage)) {
capabilities.sendMessage.forEach((msgType) => this.widgetApi.requestCapabilityToSendMessage(msgType));
}
if (capabilities.receiveMessage === true) {
this.widgetApi.requestCapabilityToReceiveMessage();
} else if (Array.isArray(capabilities.receiveMessage)) {
capabilities.receiveMessage.forEach((msgType) => this.widgetApi.requestCapabilityToReceiveMessage(msgType));
}
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
this.widgetApi.requestCapabilityToSendState(eventType, stateKey),
);
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
this.widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
);
capabilities.sendToDevice?.forEach((eventType) => this.widgetApi.requestCapabilityToSendToDevice(eventType));
capabilities.receiveToDevice?.forEach((eventType) =>
this.widgetApi.requestCapabilityToReceiveToDevice(eventType),
);
if (
capabilities.sendDelayedEvents &&
(capabilities.sendEvent?.length ||
capabilities.sendMessage === true ||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
capabilities.sendState?.length)
) {
this.widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
}
if (capabilities.updateDelayedEvents) {
this.widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
}
if (capabilities.sendSticky) {
this.widgetApi.requestCapability(MatrixCapabilities.MSC4407SendStickyEvent);
}
if (capabilities.receiveSticky) {
this.widgetApi.requestCapability(MatrixCapabilities.MSC4407ReceiveStickyEvent);
}
if (capabilities.turnServers) {
this.widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
}
}
public async supportUpdateState(): Promise<boolean> {
return (await this.widgetApi.getClientVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE);
}
private readonly syncApiResolver = Promise.withResolvers<void>();
public async startClient(opts: IStartClientOpts = {}): Promise<void> {
this.lifecycle = new AbortController();
@@ -282,6 +310,7 @@ export class RoomWidgetClient extends MatrixClient {
} else {
this.syncApi = new SyncApi(this, opts, this.buildSyncApiOptions());
}
this.syncApiResolver.resolve();
this.room = this.syncApi.createRoom(this.roomId);
this.store.storeRoom(this.room);
@@ -346,17 +375,41 @@ export class RoomWidgetClient extends MatrixClient {
throw new Error(`Unknown room: ${roomIdOrAlias}`);
}
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse>;
protected async encryptAndSendEvent(
room: Room,
event: MatrixEvent,
queryDict?: QueryDict,
): Promise<ISendEventResponse>;
protected async encryptAndSendEvent(
room: Room,
event: MatrixEvent,
delayOpts: SendDelayedEventRequestOpts,
): Promise<SendDelayedEventResponse>;
queryDict?: QueryDict,
): Promise<ISendEventResponse>;
protected async encryptAndSendEvent(
room: Room,
event: MatrixEvent,
delayOpts?: SendDelayedEventRequestOpts,
delayOptsOrQuery?: SendDelayedEventRequestOpts | QueryDict,
queryDict?: QueryDict,
): Promise<ISendEventResponse | SendDelayedEventResponse> {
let queryOpts = queryDict;
let delayOpts: SendDelayedEventRequestOpts | undefined;
if (delayOptsOrQuery && isSendDelayedEventRequestOpts(delayOptsOrQuery)) {
delayOpts = delayOptsOrQuery;
} else if (!queryOpts) {
queryOpts = delayOptsOrQuery;
}
const stickyDurationMs = queryOpts?.["org.matrix.msc4354.sticky_duration_ms"];
if (stickyDurationMs !== undefined && typeof stickyDurationMs !== "number") {
throw new Error("Sticky duration must be a number when defined");
}
// This is save since we just checked that above
// We need the additional as assertion for the EW linter to be happy.
// It is not capable of implying the type based on the throw if `stickyDurationMs !== undefined && typeof stickyDurationMs !== "number"`
// above
const stickyDurationMsAsNumber: number | undefined = stickyDurationMs as number | undefined;
// We need to extend the content with the redacts parameter
// The js sdk uses event.redacts but the widget api uses event.content.redacts
// This will be converted back to event.redacts in the widget driver.
@@ -374,6 +427,7 @@ export class RoomWidgetClient extends MatrixClient {
room.roomId,
"delay" in delayOpts ? delayOpts.delay : undefined,
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
stickyDurationMsAsNumber,
)
.catch(timeoutToConnectionError);
return this.validateSendDelayedEventResponse(response);
@@ -386,7 +440,7 @@ export class RoomWidgetClient extends MatrixClient {
let response: ISendEventFromWidgetResponseData;
try {
response = await this.widgetApi
.sendRoomEvent(event.getType(), content, room.roomId)
.sendRoomEvent(event.getType(), content, room.roomId, undefined, undefined, stickyDurationMsAsNumber)
.catch(timeoutToConnectionError);
} catch (e) {
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
@@ -683,7 +737,7 @@ export class RoomWidgetClient extends MatrixClient {
// Only inject once we have update the txId
await this.updateTxId(event);
await this.syncApiResolver.promise;
if (this.syncApi instanceof SyncApi) {
if (await this.supportUpdateState()) {
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
@@ -704,6 +758,7 @@ export class RoomWidgetClient extends MatrixClient {
}
this.emit(ClientEvent.Event, event);
if (event.unstableStickyInfo !== undefined) this.room!._unstable_addStickyEvents([event]);
this.setSyncState(SyncState.Syncing);
logger.info(`Received event ${event.getId()} ${event.getType()}`);
} else {
@@ -737,6 +792,7 @@ export class RoomWidgetClient extends MatrixClient {
"received update_state widget action but the widget driver did not claim to support 'org.matrix.msc2762_update_state'",
);
}
await this.syncApiResolver.promise;
for (const rawEvent of ev.detail.data.state) {
// Verify the room ID matches, since it's possible for the client to
// send us state updates from other rooms if this widget is always
+43 -2
View File
@@ -372,12 +372,53 @@ export class CallMembership {
*/
public get slotId(): string {
const { kind, data } = this.membershipData;
if (data.application === "m.call") {
switch (kind) {
case "rtc":
return data.slot_id;
case "session":
default: {
const [application, id] = [this.application, data.call_id];
// INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
// The spec got changed to use `"ROOM"` instead of `""` empyt string for the implicit default call.
// State events still are sent with `""` however. To find other events that should end up in the same call,
// we use the slotId.
// Since the CallMembership is the public representation of a rtc.member event, we just pretend it is a
// "ROOM" slotId/call_id.
// This makes all the remote members work with just this simple trick.
//
// We of course now need to be careful when sending legacy events (state events)
// They get a slotDescription containing "ROOM" since this is what we use starting at the time this comment
// is commited.
//
// See the Other INFO_SLOT_ID_LEGACY_CASE comments to see where we revert back to "" just before sending the event.
let compatibilityAdaptedId: string;
if (id === "") {
compatibilityAdaptedId = "ROOM";
this.logger?.info("use slotId compat hack emptyString -> ROOM");
} else {
compatibilityAdaptedId = id;
}
return slotDescriptionToId({
application,
id: compatibilityAdaptedId,
});
}
}
}
this.logger?.info("NOT using slotId compat hack emptyString -> ROOM");
// This is what the function should look like for any other application that did not
// go through a `""`=> `"ROOM"` rename
switch (kind) {
case "rtc":
return data.slot_id;
case "session":
default:
return slotDescriptionToId({ application: this.application, id: data.call_id });
default: {
const [application, id] = [this.application, data.call_id];
return slotDescriptionToId({ application, id });
}
}
}
+14 -30
View File
@@ -26,13 +26,12 @@ import { CallMembership } from "./CallMembership.ts";
import { RoomStateEvent } from "../models/room-state.ts";
import { MembershipManager, StickyEventMembershipManager } from "./MembershipManager.ts";
import { type CallMembershipIdentityParts, EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
import { deepCompare, logDurationSync } from "../utils.ts";
import { logDurationSync } from "../utils.ts";
import type {
Statistics,
RTCNotificationType,
Status,
IRTCNotificationContent,
ICallNotifyContent,
RTCCallIntent,
Transport,
} from "./types.ts";
@@ -81,7 +80,6 @@ export type MatrixRTCSessionEventHandlerMap = {
[MatrixRTCSessionEvent.MembershipManagerError]: (error: unknown) => void;
[MatrixRTCSessionEvent.DidSendCallNotification]: (
notificationContentNew: { event_id: string } & IRTCNotificationContent,
notificationContentLegacy: { event_id: string } & ICallNotifyContent,
) => void;
};
@@ -328,7 +326,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
*/
public static async sessionMembershipsForSlot(
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState" | "_unstable_getStickyEvents">,
slotDescription: SlotDescription,
slotId: string,
// default both true this implied we combine sticky and state events for the final call state
// (prefer sticky events in case of a duplicate)
options: SessionMembershipsForSlotOpts = DEFAULT_SESSION_MEMBERSHIPS_FOR_SLOT_OPTS,
@@ -339,7 +337,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
const callMemberships = await computeBackendIdentityAndVerifyMemberEvents(
room,
callMemberEvents,
slotDescription,
slotId,
logger,
);
@@ -713,20 +711,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
notificationType: RTCNotificationType,
callIntent?: RTCCallIntent,
): void {
const sendLegacyNotificationEvent = async (): Promise<{
response: ISendEventResponse;
content: ICallNotifyContent;
}> => {
const content: ICallNotifyContent = {
"application": "m.call",
"m.mentions": { user_ids: [], room: true },
"notify_type": notificationType === "notification" ? "notify" : notificationType,
"call_id": this.callId!,
};
const response = await this.client.sendEvent(this.roomSubset.roomId, EventType.CallNotify, content);
return { response, content };
};
const sendNewNotificationEvent = async (): Promise<{
const sendNotificationEvent = async (): Promise<{
response: ISendEventResponse;
content: IRTCNotificationContent;
}> => {
@@ -747,12 +732,11 @@ export class MatrixRTCSession extends TypedEventEmitter<
return { response, content };
};
void Promise.all([sendLegacyNotificationEvent(), sendNewNotificationEvent()])
.then(([legacy, newNotification]) => {
void sendNotificationEvent()
.then((notification) => {
// Join event_id and origin event content
const legacyResult = { ...legacy.response, ...legacy.content };
const newResult = { ...newNotification.response, ...newNotification.content };
this.emit(MatrixRTCSessionEvent.DidSendCallNotification, newResult, legacyResult);
const newResult = { ...notification.response, ...notification.content };
this.emit(MatrixRTCSessionEvent.DidSendCallNotification, newResult);
})
.catch(([errorLegacy, errorNew]) =>
this.logger.error("Failed to send call notification", errorLegacy, errorNew),
@@ -823,7 +807,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
this.memberships = await MatrixRTCSession.sessionMembershipsForSlot(
this.room,
this.slotDescription,
slotDescriptionToId(this.slotDescription),
this.calculateMembershipsOpts,
);
@@ -873,7 +857,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
async function computeBackendIdentityAndVerifyMemberEvents(
room: Pick<Room, "hasMembershipState">,
callMemberEvents: MatrixEvent[],
slotDescription: SlotDescription,
slotId: string,
logger: Logger,
): Promise<CallMembership[]> {
const callMemberships: CallMembership[] = [];
@@ -897,7 +881,7 @@ async function computeBackendIdentityAndVerifyMemberEvents(
logger,
);
if (isValidMembership(membership, room, slotDescription, logger)) {
if (isValidMembership(membership, room, slotId, logger)) {
callMemberships.push(membership);
}
} catch (e) {
@@ -930,12 +914,12 @@ function quickFilterNonRelevantContents(content: IContent, logger: Logger): bool
function isValidMembership(
membership: CallMembership,
room: Pick<Room, "hasMembershipState">,
slotDescription: SlotDescription,
slotId: string,
logger: Logger,
): boolean {
if (!deepCompare(membership.slotDescription, slotDescription)) {
if (membership.slotId !== slotId) {
logger.info(
`Ignoring membership of user ${membership.userId} for a different slot: ${JSON.stringify(membership.slotDescription)}`,
`Ignoring membership of user ${membership.userId} for a different slot: user: ${JSON.stringify(membership.slotDescription)}, slotId: ${slotId})`,
);
return false;
}
+1 -1
View File
@@ -56,7 +56,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
public constructor(
rootLogger: Logger,
private client: MatrixClient,
private readonly slotDescription: SlotDescription = { application: "m.call", id: "" }, // Default to the Matrix Call application
private readonly slotDescription: SlotDescription = { application: "m.call", id: "ROOM" }, // Default to the Matrix Call application
) {
super();
this.logger = rootLogger.getChild("[MatrixRTCSessionManager]");
+20 -4
View File
@@ -43,6 +43,7 @@ import {
type IMembershipManager,
type MembershipManagerEventHandlerMap,
} from "./IMembershipManager.ts";
import { isLivekitTransportConfig } from "./LivekitTransport.ts";
/* MembershipActionTypes:
On Join: (1)
@@ -584,7 +585,7 @@ export class MembershipManager
this.emit(MembershipManagerEvent.ProbablyLeft, this.state.probablyLeft);
}
private setAndEmitDelayId(delayId?: string): void {
private setAndEmitDelayId(delayId: string | undefined): void {
if (this.state.delayId === delayId) return;
this.state.delayId = delayId;
@@ -654,6 +655,7 @@ export class MembershipManager
._unstable_sendScheduledDelayedEvent(delayId)
.then(() => {
this.state.hasMemberStateEvent = false;
this.setAndEmitDelayId(undefined);
this.resetRateLimitCounter(MembershipActionType.SendScheduledDelayedLeaveEvent);
return { replace: [] };
@@ -772,7 +774,12 @@ export class MembershipManager
* which is not compatible with membershipID of session type member events. They have to be `${localUserId}:${localDeviceId}`
*/
private makeMembershipStateKey(localUserId: string, localDeviceId: string): string {
const stateKey = `${localUserId}_${localDeviceId}_${this.slotDescription.application}${this.slotDescription.id}`;
// INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
// Revert back to "" just for the state key (state keys are always legacy. we use sticky events for non legacy events)
const application = this.slotDescription.application;
const needsEmptyStringRoomFix = application === "m.call" && this.slotDescription.id === "ROOM";
const slotId = needsEmptyStringRoomFix ? "" : this.slotDescription.id;
const stateKey = `${localUserId}_${localDeviceId}_${application}${slotId}`;
if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) {
return stateKey;
} else {
@@ -785,6 +792,8 @@ export class MembershipManager
*/
protected makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData {
const ownMembership = this.ownMembership;
const needsEmptyStringRoomFix =
this.slotDescription.application === "m.call" && this.slotDescription.id === "ROOM";
const focusObjects =
this.rtcTransport === undefined
@@ -798,7 +807,9 @@ export class MembershipManager
};
return {
"application": this.slotDescription.application,
"call_id": this.slotDescription.id,
// INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
// Revert back to "" just for the sending the event.
"call_id": needsEmptyStringRoomFix ? "" : this.slotDescription.id,
"scope": "m.room",
"device_id": this.deviceId,
// DO NOT use this.memberId here since that is the state key (using application...)
@@ -1091,6 +1102,7 @@ export class StickyEventMembershipManager extends MembershipManager {
protected makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData {
const ownMembership = this.ownMembership;
const livekitTransport = isLivekitTransportConfig(this.rtcTransport) ? this.rtcTransport : undefined;
const relationObject = ownMembership?.eventId
? { "m.relation": { rel_type: RelationType.Reference, event_id: ownMembership?.eventId } }
: {};
@@ -1100,7 +1112,11 @@ export class StickyEventMembershipManager extends MembershipManager {
...(this.callIntent ? { "m.call.intent": this.callIntent } : {}),
},
slot_id: slotDescriptionToId(this.slotDescription),
rtc_transports: this.rtcTransport ? [this.rtcTransport] : [],
// Make sure we do not add the alias to the transport.
// It is not needed in matrix2.0. The additional session information will be used to find the right alias on the sfu.
rtc_transports: livekitTransport
? [{ type: livekitTransport.type, livekit_service_url: livekitTransport.livekit_service_url }]
: [],
member: { device_id: this.deviceId, user_id: this.userId, id: this.memberId },
versions: [],
...relationObject,
-1
View File
@@ -38,7 +38,6 @@ export type BeaconEventHandlerMap = {
[BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
[BeaconEvent.LocationUpdate]: (locationState: BeaconLocationState) => void;
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
};
export const isTimestampInDuration = (startTimestamp: number, durationMs: number, timestamp: number): boolean =>
+29 -2
View File
@@ -52,7 +52,34 @@ interface OidcRegistrationRequestBody {
application_type: "web" | "native";
}
export const DEVICE_CODE_SCOPE = "urn:ietf:params:oauth:grant-type:device_code";
/**
* The OAuth 2.0 grant types that are defined for Matrix in https://spec.matrix.org/v1.17/client-server-api/#grant-types
*/
export enum OAuthGrantType {
/**
* See https://spec.matrix.org/v1.17/client-server-api/#authorization-code-grant
*/
AuthorizationCode = "authorization_code",
/**
* https://spec.matrix.org/v1.17/client-server-api/#refresh-token-grant
*/
RefreshToken = "refresh_token",
/**
* The OAuth 2.0 Device Authorization Grant type identifier as per
* https://www.rfc-editor.org/rfc/rfc8628.html#section-7.2 from
* [MSC4341](https://github.com/matrix-org/matrix-spec-proposals/pull/4341).
*
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
*/
DeviceAuthorization = "urn:ietf:params:oauth:grant-type:device_code",
}
/**
* The name "scope" is a misnomer here as it is actually a "grant type".
*
* @deprecated use `OAuthGrantType.DeviceAuthorization` instead
*/
export const DEVICE_CODE_SCOPE: string = OAuthGrantType.DeviceAuthorization;
// Check that URIs have a common base, as per the MSC2966 definition
const urlHasCommonBase = (base: URL, urlStr?: string): boolean => {
@@ -79,7 +106,7 @@ export const registerOidcClient = async (
throw new Error(OidcError.DynamicRegistrationNotSupported);
}
const grantTypes: NonEmptyArray<string> = ["authorization_code", "refresh_token"];
const grantTypes: NonEmptyArray<string> = [OAuthGrantType.AuthorizationCode, OAuthGrantType.RefreshToken];
if (grantTypes.some((scope) => !delegatedAuthConfig.grant_types_supported.includes(scope))) {
throw new Error(OidcError.DynamicRegistrationNotSupported);
}
+14 -8
View File
@@ -19,13 +19,17 @@ import { type IdTokenClaims, type OidcMetadata, type SigninResponse } from "oidc
import { logger } from "../logger.ts";
import { OidcError } from "./error.ts";
import { OAuthGrantType } from "./index.ts";
/**
* Metadata from OIDC authority discovery
* Metadata from OAuth 2.0 client authentication API as per
* https://spec.matrix.org/v1.17/client-server-api/#get_matrixclientv1auth_metadata
* With validated properties required in type
*/
export type ValidatedAuthMetadata = Partial<OidcMetadata> &
Pick<
// These values are from [RFC8414](https://datatracker.ietf.org/doc/html/rfc8414#section-2)
// so we can reuse the OidcMetadata definitions from oidc-client-ts
OidcMetadata,
| "issuer"
| "authorization_endpoint"
@@ -35,12 +39,14 @@ export type ValidatedAuthMetadata = Partial<OidcMetadata> &
| "grant_types_supported"
| "code_challenge_methods_supported"
> & {
// MSC2965 extensions to the OIDC spec
// These values aren't part of RFC8414 so we add them here
// Account management fields from stable MSC4191:
account_management_uri?: string;
account_management_actions_supported?: string[];
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported`
// even though it is part of the OIDC spec
// Value from [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html):
prompt_values_supported?: string[];
// Experimental MSC4341 value from [RFC8628](https://datatracker.ietf.org/doc/html/rfc8628#section-4):
device_authorization_endpoint?: string;
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
@@ -79,9 +85,9 @@ const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, val
};
/**
* Validates issuer `.well-known/openid-configuration`
* As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html
* validates that OP is compatible with Element's OIDC flow
* Validates OAuth 2.0 auth metadata as defined by
* https://spec.matrix.org/v1.17/client-server-api/#get_matrixclientv1auth_metadata
* is compatible with Element's OAuth/OIDC flow
* @param authMetadata - json object
* @returns valid issuer config
* @throws Error - when issuer config is not found or is invalid
@@ -102,7 +108,7 @@ export const validateAuthMetadata = (authMetadata: unknown): ValidatedAuthMetada
optionalStringProperty(authMetadata, "device_authorization_endpoint"),
optionalStringArrayProperty(authMetadata, "account_management_actions_supported"),
requiredArrayValue(authMetadata, "response_types_supported", "code"),
requiredArrayValue(authMetadata, "grant_types_supported", "authorization_code"),
requiredArrayValue(authMetadata, "grant_types_supported", OAuthGrantType.AuthorizationCode),
requiredArrayValue(authMetadata, "code_challenge_methods_supported", "S256"),
optionalStringArrayProperty(authMetadata, "prompt_values_supported"),
].some((isValid) => !isValid);
+2 -2
View File
@@ -27,7 +27,7 @@ import { logger } from "../logger.ts";
import { type MSC4108SecureChannel } from "./channels/MSC4108SecureChannel.ts";
import { MatrixError } from "../http-api/index.ts";
import { sleep } from "../utils.ts";
import { DEVICE_CODE_SCOPE, type OidcClientConfig } from "../oidc/index.ts";
import { OAuthGrantType, type OidcClientConfig } from "../oidc/index.ts";
import { type CryptoApi } from "../crypto-api/index.ts";
/**
@@ -194,7 +194,7 @@ export class MSC4108SignInWithQR {
logger.error("Failed to discover OIDC metadata", e);
}
if (oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE)) {
if (oidcClientConfig?.grant_types_supported.includes(OAuthGrantType.DeviceAuthorization)) {
await this.send<ProtocolsPayload>({
type: PayloadType.Protocols,
protocols: ["device_authorization_grant"],
@@ -262,7 +262,9 @@ export class MSC4108RendezvousSession {
if (!this.url) return;
try {
await this.fetch(this.url, { method: Method.Delete });
const method = Method.Delete;
logger.info(`=> ${method} ${this.url}`);
await this.fetch(this.url, { method });
} catch (e) {
logger.warn(e);
}
+285 -271
View File
@@ -26,7 +26,7 @@ limitations under the License.
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend.ts";
import { User } from "./models/user.ts";
import { NotificationCountType, Room, RoomEvent } from "./models/room.ts";
import { deepCopy, noUnsafeEventProps, promiseMapSeries, unsafeProp } from "./utils.ts";
import { deepCopy, noUnsafeEventProps, unsafeProp } from "./utils.ts";
import { Filter } from "./filter.ts";
import { EventTimeline } from "./models/event-timeline.ts";
import { type Logger } from "./logger.ts";
@@ -1188,312 +1188,326 @@ export class SyncApi {
this.notifEvents = [];
// Handle invites
await promiseMapSeries(inviteRooms, async (inviteObj) => {
const room = inviteObj.room;
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
await Promise.all(
inviteRooms.map(async (inviteObj) => {
const room = inviteObj.room;
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
await this.injectRoomEvents(room, stateEvents, undefined);
await this.injectRoomEvents(room, stateEvents, undefined);
if (inviteObj.isBrandNewRoom) {
room.recalculate();
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
} else {
// Update room state for invite->reject->invite cycles
room.recalculate();
}
stateEvents.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
});
if (inviteObj.isBrandNewRoom) {
room.recalculate();
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
} else {
// Update room state for invite->reject->invite cycles
room.recalculate();
}
stateEvents.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
}),
);
// Handle joins
await promiseMapSeries(joinRooms, async (joinObj) => {
const room = joinObj.room;
const stateEvents = this.mapSyncEventsFormat(joinObj.state, room);
const stateAfterEvents = this.mapSyncEventsFormat(joinObj["org.matrix.msc4222.state_after"], room);
// Prevent events from being decrypted ahead of time
// this helps large account to speed up faster
// room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly
const timelineEvents = this.mapSyncEventsFormat(joinObj.timeline, room, false);
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
const stickyEvents = this.mapSyncEventsFormat(joinObj.msc4354_sticky);
await Promise.all(
joinRooms.map(async (joinObj) => {
const room = joinObj.room;
const stateEvents = this.mapSyncEventsFormat(joinObj.state, room);
const stateAfterEvents = this.mapSyncEventsFormat(joinObj["org.matrix.msc4222.state_after"], room);
// Prevent events from being decrypted ahead of time
// this helps large account to speed up faster
// room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly
const timelineEvents = this.mapSyncEventsFormat(joinObj.timeline, room, false);
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
const stickyEvents = this.mapSyncEventsFormat(joinObj.msc4354_sticky);
// If state_after is present, this is the events that form the state at the end of the timeline block and
// regular timeline events do *not* count towards state. If it's not present, then the state is formed by
// the state events plus the timeline events. Note mapSyncEventsFormat returns an empty array if the field
// is absent so we explicitly check the field on the original object.
const eventsFormingFinalState = joinObj["org.matrix.msc4222.state_after"]
? stateAfterEvents
: stateEvents.concat(timelineEvents);
// If state_after is present, this is the events that form the state at the end of the timeline block and
// regular timeline events do *not* count towards state. If it's not present, then the state is formed by
// the state events plus the timeline events. Note mapSyncEventsFormat returns an empty array if the field
// is absent so we explicitly check the field on the original object.
const eventsFormingFinalState = joinObj["org.matrix.msc4222.state_after"]
? stateAfterEvents
: stateEvents.concat(timelineEvents);
const encrypted = this.isRoomEncrypted(room, eventsFormingFinalState);
// We store the server-provided value first so it's correct when any of the events fire.
if (joinObj.unread_notifications) {
/**
* We track unread notifications ourselves in encrypted rooms, so don't
* bother setting it here. We trust our calculations better than the
* server's for this case, and therefore will assume that our non-zero
* count is accurate.
* XXX: this is known faulty as the push rule for `.m.room.encrypted` may be disabled so server
* may issue notification counts of 0 which we wrongly trust.
* https://github.com/matrix-org/matrix-spec-proposals/pull/2654 would fix this
*
* @see import("./client").fixNotificationCountOnDecryption
*/
if (!encrypted || joinObj.unread_notifications.notification_count === 0) {
// In an encrypted room, if the room has notifications enabled then it's typical for
// the server to flag all new messages as notifying. However, some push rules calculate
// events as ignored based on their event contents (e.g. ignoring msgtype=m.notice messages)
// so we want to calculate this figure on the client in all cases.
room.setUnreadNotificationCount(
NotificationCountType.Total,
joinObj.unread_notifications.notification_count ?? 0,
);
}
if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
// If the locally stored highlight count is zero, use the server provided value.
room.setUnreadNotificationCount(
NotificationCountType.Highlight,
joinObj.unread_notifications.highlight_count ?? 0,
);
}
}
const unreadThreadNotifications =
joinObj[UNREAD_THREAD_NOTIFICATIONS.name] ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName!];
if (unreadThreadNotifications) {
// This mirrors the logic above for rooms: take the *total* notification count from
// the server for unencrypted rooms or is it's zero. Any threads not present in this
// object implicitly have zero notifications, so start by clearing the total counts
// for all such threads.
room.resetThreadUnreadNotificationCountFromSync(Object.keys(unreadThreadNotifications));
for (const [threadId, unreadNotification] of Object.entries(unreadThreadNotifications)) {
if (!encrypted || unreadNotification.notification_count === 0) {
room.setThreadUnreadNotificationCount(
threadId,
const encrypted = this.isRoomEncrypted(room, eventsFormingFinalState);
// We store the server-provided value first so it's correct when any of the events fire.
if (joinObj.unread_notifications) {
/**
* We track unread notifications ourselves in encrypted rooms, so don't
* bother setting it here. We trust our calculations better than the
* server's for this case, and therefore will assume that our non-zero
* count is accurate.
* XXX: this is known faulty as the push rule for `.m.room.encrypted` may be disabled so server
* may issue notification counts of 0 which we wrongly trust.
* https://github.com/matrix-org/matrix-spec-proposals/pull/2654 would fix this
*
* @see import("./client").fixNotificationCountOnDecryption
*/
if (!encrypted || joinObj.unread_notifications.notification_count === 0) {
// In an encrypted room, if the room has notifications enabled then it's typical for
// the server to flag all new messages as notifying. However, some push rules calculate
// events as ignored based on their event contents (e.g. ignoring msgtype=m.notice messages)
// so we want to calculate this figure on the client in all cases.
room.setUnreadNotificationCount(
NotificationCountType.Total,
unreadNotification.notification_count ?? 0,
joinObj.unread_notifications.notification_count ?? 0,
);
}
const hasNoNotifications =
room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) <= 0;
if (!encrypted || (encrypted && hasNoNotifications)) {
room.setThreadUnreadNotificationCount(
threadId,
if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
// If the locally stored highlight count is zero, use the server provided value.
room.setUnreadNotificationCount(
NotificationCountType.Highlight,
unreadNotification.highlight_count ?? 0,
joinObj.unread_notifications.highlight_count ?? 0,
);
}
}
} else {
room.resetThreadUnreadNotificationCountFromSync();
}
joinObj.timeline = joinObj.timeline || ({} as ITimeline);
const unreadThreadNotifications =
joinObj[UNREAD_THREAD_NOTIFICATIONS.name] ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName!];
if (unreadThreadNotifications) {
// This mirrors the logic above for rooms: take the *total* notification count from
// the server for unencrypted rooms or is it's zero. Any threads not present in this
// object implicitly have zero notifications, so start by clearing the total counts
// for all such threads.
room.resetThreadUnreadNotificationCountFromSync(Object.keys(unreadThreadNotifications));
for (const [threadId, unreadNotification] of Object.entries(unreadThreadNotifications)) {
if (!encrypted || unreadNotification.notification_count === 0) {
room.setThreadUnreadNotificationCount(
threadId,
NotificationCountType.Total,
unreadNotification.notification_count ?? 0,
);
}
if (joinObj.isBrandNewRoom) {
// set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating.
if (joinObj.timeline.prev_batch !== null) {
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
}
} else if (joinObj.timeline.limited) {
let limited = true;
// we've got a limited sync, so we *probably* have a gap in the
// timeline, so should reset. But we might have been peeking or
// paginating and already have some of the events, in which
// case we just want to append any subsequent events to the end
// of the existing timeline.
//
// This is particularly important in the case that we already have
// *all* of the events in the timeline - in that case, if we reset
// the timeline, we'll end up with an entirely empty timeline,
// which we'll try to paginate but not get any new events (which
// will stop us linking the empty timeline into the chain).
//
for (let i = timelineEvents.length - 1; i >= 0; i--) {
const eventId = timelineEvents[i].getId()!;
if (room.getTimelineForEvent(eventId)) {
this.syncOpts.logger.debug(`Already have event ${eventId} in limited sync - not resetting`);
limited = false;
// we might still be missing some of the events before i;
// we don't want to be adding them to the end of the
// timeline because that would put them out of order.
timelineEvents.splice(0, i);
// XXX: there's a problem here if the skipped part of the
// timeline modifies the state set in stateEvents, because
// we'll end up using the state from stateEvents rather
// than the later state from timelineEvents. We probably
// need to wind stateEvents forward over the events we're
// skipping.
break;
const hasNoNotifications =
room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) <= 0;
if (!encrypted || (encrypted && hasNoNotifications)) {
room.setThreadUnreadNotificationCount(
threadId,
NotificationCountType.Highlight,
unreadNotification.highlight_count ?? 0,
);
}
}
}
if (limited) {
room.resetLiveTimeline(
joinObj.timeline.prev_batch,
this.syncOpts.canResetEntireTimeline!(room.roomId)
? null
: (syncEventData.oldSyncToken ?? null),
);
// We have to assume any gap in any timeline is
// reason to stop incrementally tracking notifications and
// reset the timeline.
client.resetNotifTimelineSet();
}
}
// process any crypto events *before* emitting the RoomStateEvent events. This
// avoids a race condition if the application tries to send a message after the
// state event is processed, but before crypto is enabled, which then causes the
// crypto layer to complain.
if (this.syncOpts.cryptoCallbacks) {
for (const e of eventsFormingFinalState) {
if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
}
}
}
// Proactively decrypt state events: normally we decrypt on demand, but for state
// events we need them immediately, so we handle them here. Specifically, consumers
// (e.g. Element Web) expect state events to be unencrypted upon receipt.
for (const ev of timelineEvents.filter((ev) => ev.isState())) {
await this.client.decryptEventIfNeeded(ev);
}
try {
if ("org.matrix.msc4222.state_after" in joinObj) {
await this.injectRoomEvents(
room,
undefined,
stateAfterEvents,
timelineEvents,
syncEventData.fromCache,
);
} else {
await this.injectRoomEvents(room, stateEvents, undefined, timelineEvents, syncEventData.fromCache);
room.resetThreadUnreadNotificationCountFromSync();
}
} catch (e) {
this.syncOpts.logger.error(`Failed to process events on room ${room.roomId}:`, e);
}
// set summary after processing events,
// because it will trigger a name calculation
// which needs the room state to be up to date
if (joinObj.summary) {
room.setSummary(joinObj.summary);
}
joinObj.timeline = joinObj.timeline || ({} as ITimeline);
// we deliberately don't add ephemeral events to the timeline
room.addEphemeralEvents(ephemeralEvents);
if (joinObj.isBrandNewRoom) {
// set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating.
if (joinObj.timeline.prev_batch !== null) {
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
}
} else if (joinObj.timeline.limited) {
let limited = true;
// we deliberately don't add accountData to the timeline
room.addAccountData(accountDataEvents);
// we've got a limited sync, so we *probably* have a gap in the
// timeline, so should reset. But we might have been peeking or
// paginating and already have some of the events, in which
// case we just want to append any subsequent events to the end
// of the existing timeline.
//
// This is particularly important in the case that we already have
// *all* of the events in the timeline - in that case, if we reset
// the timeline, we'll end up with an entirely empty timeline,
// which we'll try to paginate but not get any new events (which
// will stop us linking the empty timeline into the chain).
//
for (let i = timelineEvents.length - 1; i >= 0; i--) {
const eventId = timelineEvents[i].getId()!;
if (room.getTimelineForEvent(eventId)) {
this.syncOpts.logger.debug(`Already have event ${eventId} in limited sync - not resetting`);
limited = false;
// Sticky events primarily come via the `timeline` field, with the
// sticky info field marking them as sticky.
// If the sync is "gappy" (meaning it is skipping events to catch up) then
// sticky events will instead come down the sticky section.
// This ensures we collect sticky events from both places.
const stickyEventsAndStickyEventsFromTheTimeline = stickyEvents.concat(
timelineEvents.filter((e) => e.unstableStickyInfo !== undefined),
);
// Note: We calculate sticky events before emitting `.Room` as it's nice to have
// sticky events calculated and ready to go.
room._unstable_addStickyEvents(stickyEventsAndStickyEventsFromTheTimeline);
// we might still be missing some of the events before i;
// we don't want to be adding them to the end of the
// timeline because that would put them out of order.
timelineEvents.splice(0, i);
room.recalculate();
if (joinObj.isBrandNewRoom) {
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
}
// XXX: there's a problem here if the skipped part of the
// timeline modifies the state set in stateEvents, because
// we'll end up using the state from stateEvents rather
// than the later state from timelineEvents. We probably
// need to wind stateEvents forward over the events we're
// skipping.
this.processEventsForNotifs(room, timelineEvents);
break;
}
}
const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e);
// this fires a couple of times for some events. (eg state events are in the timeline and the state)
// should this get a sync section as an additional event emission param (e, syncSection))?
stateEvents.forEach(emitEvent);
timelineEvents.forEach(emitEvent);
ephemeralEvents.forEach(emitEvent);
accountDataEvents.forEach(emitEvent);
stickyEvents
.filter(
(stickyEvent) =>
// This is highly unlikey, but in the case where a sticky event
// has appeared in the timeline AND the sticky section, we only
// want to emit the event once.
!timelineEvents.some((timelineEvent) => timelineEvent.getId() === stickyEvent.getId()),
)
.forEach(emitEvent);
// Decrypt only the last message in all rooms to make sure we can generate a preview
// And decrypt all events after the recorded read receipt to ensure an accurate
// notification count
room.decryptCriticalEvents();
});
if (limited) {
room.resetLiveTimeline(
joinObj.timeline.prev_batch,
this.syncOpts.canResetEntireTimeline!(room.roomId)
? null
: (syncEventData.oldSyncToken ?? null),
);
// We have to assume any gap in any timeline is
// reason to stop incrementally tracking notifications and
// reset the timeline.
client.resetNotifTimelineSet();
}
}
// process any crypto events *before* emitting the RoomStateEvent events. This
// avoids a race condition if the application tries to send a message after the
// state event is processed, but before crypto is enabled, which then causes the
// crypto layer to complain.
if (this.syncOpts.cryptoCallbacks) {
for (const e of eventsFormingFinalState) {
if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
}
}
}
// Proactively decrypt state events: normally we decrypt on demand, but for state
// events we need them immediately, so we handle them here. Specifically, consumers
// (e.g. Element Web) expect state events to be unencrypted upon receipt.
for (const ev of timelineEvents.filter((ev) => ev.isState())) {
await this.client.decryptEventIfNeeded(ev);
}
try {
if ("org.matrix.msc4222.state_after" in joinObj) {
await this.injectRoomEvents(
room,
undefined,
stateAfterEvents,
timelineEvents,
syncEventData.fromCache,
);
} else {
await this.injectRoomEvents(
room,
stateEvents,
undefined,
timelineEvents,
syncEventData.fromCache,
);
}
} catch (e) {
this.syncOpts.logger.error(`Failed to process events on room ${room.roomId}:`, e);
}
// set summary after processing events,
// because it will trigger a name calculation
// which needs the room state to be up to date
if (joinObj.summary) {
room.setSummary(joinObj.summary);
}
// we deliberately don't add ephemeral events to the timeline
room.addEphemeralEvents(ephemeralEvents);
// we deliberately don't add accountData to the timeline
room.addAccountData(accountDataEvents);
// Sticky events primarily come via the `timeline` field, with the
// sticky info field marking them as sticky.
// If the sync is "gappy" (meaning it is skipping events to catch up) then
// sticky events will instead come down the sticky section.
// This ensures we collect sticky events from both places.
const stickyEventsAndStickyEventsFromTheTimeline = stickyEvents.concat(
timelineEvents.filter((e) => e.unstableStickyInfo !== undefined),
);
// Note: We calculate sticky events before emitting `.Room` as it's nice to have
// sticky events calculated and ready to go.
room._unstable_addStickyEvents(stickyEventsAndStickyEventsFromTheTimeline);
room.recalculate();
if (joinObj.isBrandNewRoom) {
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
}
this.processEventsForNotifs(room, timelineEvents);
const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e);
// this fires a couple of times for some events. (eg state events are in the timeline and the state)
// should this get a sync section as an additional event emission param (e, syncSection))?
stateEvents.forEach(emitEvent);
timelineEvents.forEach(emitEvent);
ephemeralEvents.forEach(emitEvent);
accountDataEvents.forEach(emitEvent);
stickyEvents
.filter(
(stickyEvent) =>
// This is highly unlikey, but in the case where a sticky event
// has appeared in the timeline AND the sticky section, we only
// want to emit the event once.
!timelineEvents.some((timelineEvent) => timelineEvent.getId() === stickyEvent.getId()),
)
.forEach(emitEvent);
// Decrypt only the last message in all rooms to make sure we can generate a preview
// And decrypt all events after the recorded read receipt to ensure an accurate
// notification count
room.decryptCriticalEvents();
}),
);
// Handle leaves (e.g. kicked rooms)
await promiseMapSeries(leaveRooms, async (leaveObj) => {
const room = leaveObj.room;
const { timelineEvents, stateEvents, stateAfterEvents } = await this.mapAndInjectRoomEvents(leaveObj);
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
await Promise.all(
leaveRooms.map(async (leaveObj) => {
const room = leaveObj.room;
const { timelineEvents, stateEvents, stateAfterEvents } = await this.mapAndInjectRoomEvents(leaveObj);
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
room.addAccountData(accountDataEvents);
room.addAccountData(accountDataEvents);
room.recalculate();
if (leaveObj.isBrandNewRoom) {
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
}
room.recalculate();
if (leaveObj.isBrandNewRoom) {
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
}
this.processEventsForNotifs(room, timelineEvents);
this.processEventsForNotifs(room, timelineEvents);
stateEvents?.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
stateAfterEvents?.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
timelineEvents.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
accountDataEvents.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
});
stateEvents?.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
stateAfterEvents?.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
timelineEvents.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
accountDataEvents.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
}),
);
// Handle knocks
await promiseMapSeries(knockRooms, async (knockObj) => {
const room = knockObj.room;
const stateEvents = this.mapSyncEventsFormat(knockObj.knock_state, room);
await Promise.all(
knockRooms.map(async (knockObj) => {
const room = knockObj.room;
const stateEvents = this.mapSyncEventsFormat(knockObj.knock_state, room);
await this.injectRoomEvents(room, stateEvents, undefined);
await this.injectRoomEvents(room, stateEvents, undefined);
if (knockObj.isBrandNewRoom) {
room.recalculate();
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
} else {
// Update room state for knock->leave->knock cycles
room.recalculate();
}
stateEvents.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
});
if (knockObj.isBrandNewRoom) {
room.recalculate();
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
} else {
// Update room state for knock->leave->knock cycles
room.recalculate();
}
stateEvents.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
}),
);
// update the notification timeline, if appropriate.
// we only do this for live events, as otherwise we can't order them sanely
+2 -2
View File
@@ -27,7 +27,7 @@ import { type RoomMember } from "./models/room-member.ts";
import { EventType } from "./@types/event.ts";
import { type DecryptionFailureCode } from "./crypto-api/index.ts";
import { DecryptionError, type EventDecryptionResult } from "./common-crypto/CryptoBackend.ts";
import { type OidcClientConfig, type ValidatedAuthMetadata } from "./oidc/index.ts";
import { OAuthGrantType, type OidcClientConfig, type ValidatedAuthMetadata } from "./oidc/index.ts";
/**
* Create a {@link MatrixEvent}.
@@ -226,6 +226,6 @@ export const mockOpenIdConfiguration = (
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
grant_types_supported: [OAuthGrantType.AuthorizationCode, OAuthGrantType.RefreshToken, ...additionalGrantTypes],
code_challenge_methods_supported: ["S256"],
});
-1
View File
@@ -11,7 +11,6 @@
"strict": true,
"allowImportingTsExtensions": true,
"lib": ["es2024", "dom", "dom.iterable"],
"baseUrl": ".",
"types": ["vitest/globals", "node"],
"paths": {
"#types/*": ["./node_modules/vite/types/*"]
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/tsdoc/v0/tsdoc.schema.json",
"extends": ["typedoc/tsdoc.json"],
"noStandardTags": false,
"tagDefinitions": [
{
"tagName": "@alias",
"syntaxKind": "modifier"
}
]
}
+1 -1
View File
@@ -15,7 +15,7 @@ const slowTestReporter: Reporter = {
onTestRunEnd(testModules, unhandledErrors, reason) {
const tests = testModules
.flatMap((m) => Array.from(m.children.allTests()))
.filter((test) => test.diagnostic().slow);
.filter((test) => test.diagnostic()?.slow);
tests.sort((x, y) => x.diagnostic()?.duration! - y.diagnostic()?.duration!);
tests.reverse();
-5908
View File
File diff suppressed because it is too large Load Diff