Compare commits
19 Commits
v31.4.0
...
v31.5.0-rc.0
| Author | SHA1 | Date | |
|---|---|---|---|
| ee8413beff | |||
| e4d4628cc8 | |||
| b2e09250d9 | |||
| 6176faef48 | |||
| 453cdd9eda | |||
| d3dfcd9242 | |||
| a26fc46ed4 | |||
| be3913e8a5 | |||
| c1e0192baf | |||
| 8123e9a3f1 | |||
| 0425f4e5c8 | |||
| 5b74b446d4 | |||
| 624914a565 | |||
| 4da9627727 | |||
| 1cb30bfe9b | |||
| 12308b4c07 | |||
| 7f25162725 | |||
| 4826868a8f | |||
| 91bde6afa1 |
@@ -16,7 +16,7 @@ concurrency:
|
||||
jobs:
|
||||
build-element-web:
|
||||
name: Build element-web
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.91.0
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.92.0
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
|
||||
@@ -46,6 +46,7 @@ jobs:
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
if: github.event.pull_request.user.login != 'renovate[bot]'
|
||||
uses: tspascoal/get-user-teams-membership@ba78054988f58bea69b7c6136d563236f8ed2fc0 # v3
|
||||
id: teams
|
||||
with:
|
||||
@@ -55,7 +56,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- name: Add label
|
||||
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
|
||||
if: steps.teams.outputs.isTeamMember == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
|
||||
@@ -63,6 +63,7 @@ jobs:
|
||||
yarn tpv purge --yes --out _docs --stale --major 10
|
||||
yarn gendoc
|
||||
symlinks -rc _docs
|
||||
find _docs -xtype l -delete
|
||||
|
||||
- name: 🔨 Set up git
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Move new issues into Issue triage board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
automate-project-columns-next:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
project-url: https://github.com/orgs/element-hq/projects/120
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "31.4.0",
|
||||
"version": "31.5.0-rc.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -52,7 +52,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^4.5.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^4.6.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
@@ -92,8 +92,8 @@
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "9",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"domexception": "^4.0.0",
|
||||
@@ -106,11 +106,11 @@
|
||||
"eslint-plugin-jsdoc": "^48.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^50.0.0",
|
||||
"eslint-plugin-unicorn": "^51.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "9.11.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"husky": "^8.0.3",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
@@ -118,11 +118,11 @@
|
||||
"lint-staged": "^15.0.2",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.2.4",
|
||||
"prettier": "3.2.5",
|
||||
"rimraf": "^5.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.24.0",
|
||||
"typedoc-plugin-coverage": "^2.1.0",
|
||||
"typedoc-plugin-coverage": "^3.0.0",
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
"typedoc-plugin-missing-exports": "^2.0.0",
|
||||
"typedoc-plugin-versions": "^0.2.3",
|
||||
|
||||
@@ -2,23 +2,28 @@
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
|
||||
// Or it can be a string in the form owner/repo@tag
|
||||
// Or it can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
|
||||
async function listReleases(github, owner, repo) {
|
||||
const response = await github.rest.repos.listReleases({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
});
|
||||
// Filters out draft releases
|
||||
return response.data.filter((release) => !release.draft);
|
||||
}
|
||||
|
||||
// Dependency can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
|
||||
// Or it can be a string in the form accepted by `getRelease`
|
||||
async function getReleases(github, dependency) {
|
||||
if (Array.isArray(dependency)) {
|
||||
const [dep, fromVersion, toVersion] = dependency;
|
||||
const upstreamPackageJson = getDependencyPackageJson(dep);
|
||||
const [owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
|
||||
|
||||
const response = await github.rest.repos.listReleases({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
});
|
||||
const unfilteredReleases = await listReleases(github, owner, repo);
|
||||
// Only include non-draft & non-prerelease releases, unless the to-release is a pre-release, include that one
|
||||
const releases = response.data.filter(
|
||||
(release) => !release.draft && (!release.prerelease || release.tag_name === `v${toVersion}`),
|
||||
const releases = unfilteredReleases.filter(
|
||||
(release) => !release.prerelease || release.tag_name === `v${toVersion}`,
|
||||
);
|
||||
|
||||
const fromVersionIndex = releases.findIndex((release) => release.tag_name === `v${fromVersion}`);
|
||||
@@ -30,14 +35,35 @@ async function getReleases(github, dependency) {
|
||||
return [await getRelease(github, dependency)];
|
||||
}
|
||||
|
||||
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
|
||||
// Or it can be a string in the form owner/repo@tag - in this case the tag is used exactly to find the release
|
||||
// Or it can be a string in the form owner/repo~tag - in this case the latest tag in the same major.minor.patch set is used to find the release
|
||||
async function getRelease(github, dependency) {
|
||||
let owner;
|
||||
let repo;
|
||||
let tag;
|
||||
if (dependency.includes("/") && dependency.includes("@")) {
|
||||
owner = dependency.split("/")[0];
|
||||
repo = dependency.split("/")[1].split("@")[0];
|
||||
tag = dependency.split("@")[1];
|
||||
|
||||
if (dependency.includes("/")) {
|
||||
let rest;
|
||||
[owner, rest] = dependency.split("/")[0];
|
||||
|
||||
if (dependency.includes("@")) {
|
||||
[repo, tag] = rest.split("@");
|
||||
} else if (dependency.includes("~")) {
|
||||
[repo, tag] = rest.split("~");
|
||||
|
||||
if (tag.includes("-rc.")) {
|
||||
// If the tag is an RC, find the latest matching RC in the set
|
||||
try {
|
||||
const releases = await listReleases(github, owner, repo);
|
||||
const baseVersion = tag.split("-rc.")[0];
|
||||
const release = releases.find((release) => release.tag_name.startsWith(baseVersion));
|
||||
if (release) return release;
|
||||
} catch (e) {
|
||||
// Fall back to getReleaseByTag
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const upstreamPackageJson = getDependencyPackageJson(dependency);
|
||||
[owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
|
||||
@@ -129,7 +155,7 @@ if (require.main === module) {
|
||||
process.exit(1);
|
||||
}
|
||||
const [releaseId, ...dependencies] = process.argv.slice(2);
|
||||
main({ github, releaseId, dependencies: ["matrix-react-sdk"] }).then((output) => {
|
||||
main({ github, releaseId, dependencies }).then((output) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(output);
|
||||
});
|
||||
|
||||
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { OidcClientConfig } from "../../src";
|
||||
import { ValidatedIssuerMetadata } from "../../src/oidc/validate";
|
||||
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
|
||||
|
||||
/**
|
||||
* Makes a valid OidcClientConfig with minimum valid values
|
||||
@@ -26,8 +25,7 @@ export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClien
|
||||
const metadata = mockOpenIdConfiguration(issuer);
|
||||
|
||||
return {
|
||||
issuer,
|
||||
account: issuer + "account",
|
||||
accountManagementEndpoint: issuer + "account",
|
||||
registrationEndpoint: metadata.registration_endpoint,
|
||||
authorizationEndpoint: metadata.authorization_endpoint,
|
||||
tokenEndpoint: metadata.token_endpoint,
|
||||
|
||||
@@ -15,13 +15,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { AutoDiscoveryAction, M_AUTHENTICATION } from "../../src";
|
||||
import { AutoDiscoveryAction } from "../../src";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
import { OidcError } from "../../src/oidc/error";
|
||||
import { makeDelegatedAuthConfig } from "../test-utils/oidc";
|
||||
|
||||
// keep to reset the fetch function after using MockHttpBackend
|
||||
// @ts-ignore private property
|
||||
@@ -409,10 +406,6 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -450,10 +443,6 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -476,9 +465,6 @@ describe("AutoDiscovery", function () {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.authentication": {
|
||||
invalid: true,
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
@@ -494,10 +480,6 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "FAIL_ERROR",
|
||||
error: OidcError.Misconfigured,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -728,10 +710,6 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -784,10 +762,6 @@ describe("AutoDiscovery", function () {
|
||||
"org.example.custom.property": {
|
||||
cupcakes: "yes",
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -897,75 +871,4 @@ describe("AutoDiscovery", function () {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("m.authentication", () => {
|
||||
const homeserverName = "example.org";
|
||||
const homeserverUrl = "https://chat.example.org/";
|
||||
const issuer = "https://auth.org/";
|
||||
|
||||
beforeAll(() => {
|
||||
// make these tests independent from fetch mocking above
|
||||
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.resetBehavior();
|
||||
fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["v1.5"] });
|
||||
|
||||
fetchMock.get("https://example.org/.well-known/matrix/client", {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.authentication": {
|
||||
issuer,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return valid authentication configuration", async () => {
|
||||
const config = makeDelegatedAuthConfig(issuer);
|
||||
|
||||
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
|
||||
fetchMock.get(`${config.metadata.issuer}jwks`, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
|
||||
const result = await AutoDiscovery.findClientConfig(homeserverName);
|
||||
|
||||
expect(result[M_AUTHENTICATION.stable!]).toEqual({
|
||||
state: AutoDiscovery.SUCCESS,
|
||||
...config,
|
||||
signingKeys: [],
|
||||
account: undefined,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("should set state to error for invalid authentication configuration", async () => {
|
||||
const config = makeDelegatedAuthConfig(issuer);
|
||||
// authorization_code is required
|
||||
config.metadata.grant_types_supported = ["openid"];
|
||||
|
||||
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
|
||||
fetchMock.get(`${config.metadata.issuer}jwks`, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
|
||||
const result = await AutoDiscovery.findClientConfig(homeserverName);
|
||||
|
||||
expect(result[M_AUTHENTICATION.stable!]).toEqual({
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: OidcError.OpSupport,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,8 +46,8 @@ const realSubtleCrypto = crypto.subtleCrypto;
|
||||
|
||||
describe("oidc authorization", () => {
|
||||
const delegatedAuthConfig = makeDelegatedAuthConfig();
|
||||
const authorizationEndpoint = delegatedAuthConfig.metadata.authorization_endpoint;
|
||||
const tokenEndpoint = delegatedAuthConfig.metadata.token_endpoint;
|
||||
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
|
||||
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
|
||||
const clientId = "xyz789";
|
||||
const baseUrl = "https://test.com";
|
||||
|
||||
@@ -58,7 +58,10 @@ describe("oidc authorization", () => {
|
||||
jest.spyOn(logger, "warn");
|
||||
jest.setSystemTime(now);
|
||||
|
||||
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
|
||||
fetchMock.get(
|
||||
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
|
||||
mockOpenIdConfiguration(),
|
||||
);
|
||||
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: {
|
||||
|
||||
@@ -18,10 +18,10 @@ import fetchMockJest from "fetch-mock-jest";
|
||||
|
||||
import { OidcError } from "../../../src/oidc/error";
|
||||
import { OidcRegistrationClientMetadata, registerOidcClient } from "../../../src/oidc/register";
|
||||
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
|
||||
describe("registerOidcClient()", () => {
|
||||
const issuer = "https://auth.com/";
|
||||
const registrationEndpoint = "https://auth.com/register";
|
||||
const clientName = "Element";
|
||||
const baseUrl = "https://just.testing";
|
||||
const metadata: OidcRegistrationClientMetadata = {
|
||||
@@ -35,25 +35,20 @@ describe("registerOidcClient()", () => {
|
||||
};
|
||||
const dynamicClientId = "xyz789";
|
||||
|
||||
const delegatedAuthConfig = {
|
||||
issuer,
|
||||
registrationEndpoint,
|
||||
authorizationEndpoint: issuer + "auth",
|
||||
tokenEndpoint: issuer + "token",
|
||||
};
|
||||
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
|
||||
beforeEach(() => {
|
||||
fetchMockJest.mockClear();
|
||||
fetchMockJest.resetBehavior();
|
||||
});
|
||||
|
||||
it("should make correct request to register client", async () => {
|
||||
fetchMockJest.post(registrationEndpoint, {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
status: 200,
|
||||
body: JSON.stringify({ client_id: dynamicClientId }),
|
||||
});
|
||||
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
|
||||
expect(fetchMockJest).toHaveBeenCalledWith(
|
||||
registrationEndpoint,
|
||||
delegatedAuthConfig.registrationEndpoint!,
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
@@ -77,7 +72,7 @@ describe("registerOidcClient()", () => {
|
||||
});
|
||||
|
||||
it("should throw when registration request fails", async () => {
|
||||
fetchMockJest.post(registrationEndpoint, {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
status: 500,
|
||||
});
|
||||
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
|
||||
@@ -86,7 +81,7 @@ describe("registerOidcClient()", () => {
|
||||
});
|
||||
|
||||
it("should throw when registration response is invalid", async () => {
|
||||
fetchMockJest.post(registrationEndpoint, {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
status: 200,
|
||||
// no clientId in response
|
||||
body: "{}",
|
||||
|
||||
@@ -64,7 +64,7 @@ describe("OidcTokenRefresher", () => {
|
||||
keys: [],
|
||||
});
|
||||
|
||||
fetchMock.post(config.metadata.token_endpoint, {
|
||||
fetchMock.post(config.tokenEndpoint, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -88,7 +88,7 @@ describe("OidcTokenRefresher", () => {
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await expect(refresher.oidcClientReady).rejects.toThrow();
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"Failed to initialise OIDC client.",
|
||||
@@ -98,7 +98,7 @@ describe("OidcTokenRefresher", () => {
|
||||
});
|
||||
|
||||
it("initialises oidc client", async () => {
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
|
||||
// @ts-ignore peek at private property to see we initialised the client correctly
|
||||
@@ -114,19 +114,19 @@ describe("OidcTokenRefresher", () => {
|
||||
|
||||
describe("doRefreshAccessToken()", () => {
|
||||
it("should throw when oidcClient has not been initialised", async () => {
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow(
|
||||
"Cannot get new token before OIDC client is initialised.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should refresh the tokens", async () => {
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
|
||||
const result = await refresher.doRefreshAccessToken("refresh-token");
|
||||
|
||||
expect(fetchMock).toHaveFetched(config.metadata.token_endpoint, {
|
||||
expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
@@ -137,7 +137,7 @@ describe("OidcTokenRefresher", () => {
|
||||
});
|
||||
|
||||
it("should persist the new tokens", async () => {
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
// spy on our stub
|
||||
jest.spyOn(refresher, "persistTokens");
|
||||
@@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => {
|
||||
it("should only have one inflight refresh request at once", async () => {
|
||||
fetchMock
|
||||
.postOnce(
|
||||
config.metadata.token_endpoint,
|
||||
config.tokenEndpoint,
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
@@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: true },
|
||||
)
|
||||
.postOnce(
|
||||
config.metadata.token_endpoint,
|
||||
config.tokenEndpoint,
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
@@ -175,7 +175,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: false },
|
||||
);
|
||||
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
// reset call counts
|
||||
fetchMock.resetHistory();
|
||||
@@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => {
|
||||
const result2 = await first;
|
||||
|
||||
// only one call to token endpoint
|
||||
expect(fetchMock).toHaveFetchedTimes(1, config.metadata.token_endpoint);
|
||||
expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint);
|
||||
expect(result1).toEqual({
|
||||
accessToken: "first-new-access-token",
|
||||
refreshToken: "first-new-refresh-token",
|
||||
@@ -208,7 +208,7 @@ describe("OidcTokenRefresher", () => {
|
||||
|
||||
it("should log and rethrow when token refresh fails", async () => {
|
||||
fetchMock.post(
|
||||
config.metadata.token_endpoint,
|
||||
config.tokenEndpoint,
|
||||
{
|
||||
status: 503,
|
||||
headers: {
|
||||
@@ -218,7 +218,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
|
||||
await expect(refresher.doRefreshAccessToken("refresh-token")).rejects.toThrow();
|
||||
@@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => {
|
||||
// make sure inflight request is cleared after a failure
|
||||
fetchMock
|
||||
.postOnce(
|
||||
config.metadata.token_endpoint,
|
||||
config.tokenEndpoint,
|
||||
{
|
||||
status: 503,
|
||||
headers: {
|
||||
@@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: true },
|
||||
)
|
||||
.postOnce(
|
||||
config.metadata.token_endpoint,
|
||||
config.tokenEndpoint,
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
@@ -249,7 +249,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: false },
|
||||
);
|
||||
|
||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||
await refresher.oidcClientReady;
|
||||
// reset call counts
|
||||
fetchMock.resetHistory();
|
||||
|
||||
@@ -17,105 +17,12 @@ limitations under the License.
|
||||
import { mocked } from "jest-mock";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
import { M_AUTHENTICATION } from "../../../src";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
validateIdToken,
|
||||
validateOIDCIssuerWellKnown,
|
||||
validateWellKnownAuthentication,
|
||||
} from "../../../src/oidc/validate";
|
||||
import { validateIdToken, validateOIDCIssuerWellKnown } from "../../../src/oidc/validate";
|
||||
import { OidcError } from "../../../src/oidc/error";
|
||||
|
||||
jest.mock("jwt-decode");
|
||||
|
||||
describe("validateWellKnownAuthentication()", () => {
|
||||
const baseWk = {
|
||||
"m.homeserver": {
|
||||
base_url: "https://hs.org",
|
||||
},
|
||||
};
|
||||
it("should throw not supported error when wellKnown has no m.authentication section", () => {
|
||||
expect(() => validateWellKnownAuthentication(undefined)).toThrow(OidcError.NotSupported);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication issuer is not a string", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: { url: "test.com" },
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
|
||||
OidcError.Misconfigured,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication account is not a string", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
account: { url: "test" },
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
|
||||
OidcError.Misconfigured,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication account is false", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
account: false,
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
|
||||
OidcError.Misconfigured,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return valid config when wk uses stable m.authentication", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
account: "account.com",
|
||||
},
|
||||
};
|
||||
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
|
||||
issuer: "test.com",
|
||||
account: "account.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return valid config when m.authentication account is missing", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
},
|
||||
};
|
||||
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
|
||||
issuer: "test.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should remove unexpected properties", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
somethingElse: "test",
|
||||
},
|
||||
};
|
||||
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
|
||||
issuer: "test.com",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateOIDCIssuerWellKnown", () => {
|
||||
const validWk: any = {
|
||||
authorization_endpoint: "https://test.org/authorize",
|
||||
|
||||
@@ -27,8 +27,9 @@ import {
|
||||
UploadSigningKeysRequest,
|
||||
ToDeviceRequest,
|
||||
} from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { TypedEventEmitter } from "../../../src";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, MatrixHttpApi, UIAuthCallback } from "../../../src";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import { defer } from "../../../src/utils";
|
||||
@@ -274,4 +275,302 @@ describe("OutgoingRequestProcessor", () => {
|
||||
// ... and `makeOutgoingRequest` resolves satisfactorily
|
||||
await result;
|
||||
});
|
||||
|
||||
describe("Should retry requests", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
// here we use another httpApi instance in order to use fetchMock
|
||||
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||
baseUrl: "https://example.com",
|
||||
prefix: "/_matrix",
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
processor = new OutgoingRequestProcessor(olmMachine, httpApi);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
describe("Should retry on retryable errors", () => {
|
||||
const retryableErrors: Array<[number, { status: number; body: { error: string } }]> = [
|
||||
[429, { status: 429, body: { error: "Too Many Requests" } }],
|
||||
[500, { status: 500, body: { error: "Internal Server Error" } }],
|
||||
[502, { status: 502, body: { error: "Bad Gateway" } }],
|
||||
[503, { status: 503, body: { error: "Service Unavailable" } }],
|
||||
[504, { status: 504, body: { error: "Gateway timeout" } }],
|
||||
[505, { status: 505, body: { error: "HTTP Version Not Supported" } }],
|
||||
[506, { status: 506, body: { error: "Variant Also Negotiates" } }],
|
||||
[507, { status: 507, body: { error: "Insufficient Storage" } }],
|
||||
[508, { status: 508, body: { error: "Loop Detected" } }],
|
||||
[510, { status: 510, body: { error: "Not Extended" } }],
|
||||
[511, { status: 511, body: { error: "Network Authentication Required" } }],
|
||||
[525, { status: 525, body: { error: "SSL Handshake Failed" } }],
|
||||
];
|
||||
describe.each(retryableErrors)(`When status code is %s`, (_, error) => {
|
||||
test.each(tests)(`for request of type %ss`, async (_, RequestClass, expectedMethod, expectedPath) => {
|
||||
// first, mock up a request as we might expect to receive it from the Rust layer ...
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new RequestClass("1234", testBody);
|
||||
|
||||
fetchMock.mock(expectedPath, error, { method: expectedMethod });
|
||||
|
||||
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
// Run all timers and wait for the request promise to resolve/reject
|
||||
await Promise.all([jest.runAllTimersAsync(), requestPromise.catch(() => {})]);
|
||||
|
||||
await expect(requestPromise).rejects.toThrow();
|
||||
|
||||
// Should have ultimately made 5 requests (1 initial + 4 retries)
|
||||
const calls = fetchMock.calls(expectedPath);
|
||||
expect(calls).toHaveLength(5);
|
||||
|
||||
// The promise should have been rejected
|
||||
await expect(requestPromise).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should not retry if M_TOO_LARGE", async () => {
|
||||
const testBody = '{ "messages": { "user": {"device": "bar" }}}';
|
||||
const outgoingRequest = new ToDeviceRequest("1234", "custom.type", "12345", testBody);
|
||||
|
||||
fetchMock.put("express:/_matrix/client/v3/sendToDevice/:type/:txnId", {
|
||||
status: 502,
|
||||
body: {
|
||||
errcode: "M_TOO_LARGE",
|
||||
error: "Request too large",
|
||||
},
|
||||
});
|
||||
|
||||
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
await Promise.all([requestPromise.catch(() => {}), jest.runAllTimersAsync()]);
|
||||
|
||||
await expect(requestPromise).rejects.toThrow();
|
||||
|
||||
const calls = fetchMock.calls("express:/_matrix/client/v3/sendToDevice/:type/:txnId");
|
||||
expect(calls).toHaveLength(1);
|
||||
|
||||
// The promise should have been rejected
|
||||
await expect(requestPromise).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should retry on Failed to fetch connection errors", async () => {
|
||||
let callCount = 0;
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/upload", (url, opts) => {
|
||||
callCount++;
|
||||
if (callCount == 2) {
|
||||
return {
|
||||
status: 200,
|
||||
body: "{}",
|
||||
};
|
||||
} else {
|
||||
throw new Error("Failed to fetch");
|
||||
}
|
||||
});
|
||||
|
||||
const outgoingRequest = new KeysUploadRequest("1234", "{}");
|
||||
|
||||
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
|
||||
|
||||
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should retry to send to-device", async () => {
|
||||
let callCount = 0;
|
||||
const testBody = '{ "messages": { "user": {"device": "bar" }}}';
|
||||
const outgoingRequest = new ToDeviceRequest("1234", "custom.type", "12345", testBody);
|
||||
|
||||
fetchMock.put("express:/_matrix/client/v3/sendToDevice/:type/:txnId", (url, opts) => {
|
||||
callCount++;
|
||||
if (callCount == 2) {
|
||||
return {
|
||||
status: 200,
|
||||
body: "{}",
|
||||
};
|
||||
} else {
|
||||
throw new Error("Failed to fetch");
|
||||
}
|
||||
});
|
||||
|
||||
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
|
||||
|
||||
const calls = fetchMock.calls("express:/_matrix/client/v3/sendToDevice/:type/:txnId");
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should retry to call with UIA", async () => {
|
||||
let callCount = 0;
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new UploadSigningKeysRequest(testBody);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", (url, opts) => {
|
||||
callCount++;
|
||||
if (callCount == 2) {
|
||||
return {
|
||||
status: 200,
|
||||
body: "{}",
|
||||
};
|
||||
} else {
|
||||
throw new Error("Failed to fetch");
|
||||
}
|
||||
});
|
||||
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
|
||||
return await makeRequest({ type: "test" });
|
||||
};
|
||||
const requestPromise = processor.makeOutgoingRequest(outgoingRequest, authCallback);
|
||||
|
||||
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
|
||||
|
||||
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/device_signing/upload");
|
||||
expect(calls).toHaveLength(2);
|
||||
// Will not mark as sent as it's a UIA request
|
||||
});
|
||||
|
||||
it("should retry on respect server cool down on LIMIT_EXCEEDED", async () => {
|
||||
const retryAfterMs = 5000;
|
||||
let callCount = 0;
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/upload", (url, opts) => {
|
||||
callCount++;
|
||||
if (callCount == 2) {
|
||||
return {
|
||||
status: 200,
|
||||
body: "{}",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 429,
|
||||
body: {
|
||||
errcode: "M_LIMIT_EXCEEDED",
|
||||
error: "Too many requests",
|
||||
retry_after_ms: retryAfterMs,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const outgoingRequest = new KeysUploadRequest("1234", "{}");
|
||||
|
||||
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
// advanced by less than the retryAfterMs
|
||||
await jest.advanceTimersByTimeAsync(retryAfterMs - 1000);
|
||||
|
||||
// should not have made a second request yet
|
||||
{
|
||||
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
|
||||
expect(calls).toHaveLength(1);
|
||||
}
|
||||
|
||||
// advanced by the remaining time
|
||||
await jest.advanceTimersByTimeAsync(retryAfterMs + 1000);
|
||||
|
||||
await requestPromise;
|
||||
|
||||
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const nonRetryableErrors: Array<[number, { status: number; body: { errcode: string } }]> = [
|
||||
[400, { status: 400, body: { errcode: "Bad Request" } }],
|
||||
[401, { status: 401, body: { errcode: "M_UNKNOWN_TOKEN" } }],
|
||||
[403, { status: 403, body: { errcode: "M_FORBIDDEN" } }],
|
||||
];
|
||||
|
||||
describe.each(nonRetryableErrors)("Should not retry all sort of errors", (_, error) => {
|
||||
test.each(tests)("for %ss", async (_, RequestClass, expectedMethod, expectedPath) => {
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new RequestClass("1234", testBody);
|
||||
|
||||
// @ts-ignore to avoid having to do if else to switch the method (.put/.post)
|
||||
fetchMock[expectedMethod.toLowerCase()](expectedPath, {
|
||||
status: error.status,
|
||||
body: error.body,
|
||||
});
|
||||
|
||||
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
// Run all timers and wait for the request promise to resolve/reject
|
||||
await Promise.all([jest.runAllTimersAsync(), requestPromise.catch(() => {})]);
|
||||
|
||||
await expect(requestPromise).rejects.toThrow();
|
||||
|
||||
// Should have only tried once
|
||||
const calls = fetchMock.calls(expectedPath);
|
||||
expect(calls).toHaveLength(1);
|
||||
|
||||
await expect(requestPromise).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Should not retry client timeouts", () => {
|
||||
test.each(tests)("for %ss", async (_, RequestClass, expectedMethod, expectedPath) => {
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new RequestClass("1234", testBody);
|
||||
|
||||
// @ts-ignore to avoid having to do if else to switch the method (.put/.post)
|
||||
fetchMock[expectedMethod.toLowerCase()](expectedPath, () => {
|
||||
// This is what a client timeout error will throw
|
||||
throw new DOMException("The user aborted a request.", "AbortError");
|
||||
});
|
||||
|
||||
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
// Run all timers and wait for the request promise to resolve/reject
|
||||
await Promise.all([jest.runAllTimersAsync(), requestPromise.catch(() => {})]);
|
||||
|
||||
await expect(requestPromise).rejects.toThrow();
|
||||
|
||||
// Should have only tried once
|
||||
const calls = fetchMock.calls(expectedPath);
|
||||
expect(calls).toHaveLength(1);
|
||||
await expect(requestPromise).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Should retry until it works", () => {
|
||||
it.each([1, 2, 3, 4])("should succeed if the call number %s is ok", async (successfulCall) => {
|
||||
let callCount = 0;
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/upload", (url, opts) => {
|
||||
callCount++;
|
||||
if (callCount == successfulCall) {
|
||||
return {
|
||||
status: 200,
|
||||
body: "{}",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 500,
|
||||
body: { error: "Internal server error" },
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const outgoingRequest = new KeysUploadRequest("1234", "{}");
|
||||
|
||||
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
|
||||
|
||||
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
|
||||
expect(calls).toHaveLength(successfulCall);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-125
@@ -15,19 +15,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SigningKey } from "oidc-client-ts";
|
||||
|
||||
import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, IServerVersions, M_AUTHENTICATION } from "./client";
|
||||
import { IClientWellKnown, IWellKnownConfig, IServerVersions } from "./client";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixError, Method, timeoutSignal } from "./http-api";
|
||||
import { discoverAndValidateAuthenticationConfig } from "./oidc/discovery";
|
||||
import {
|
||||
ValidatedIssuerConfig,
|
||||
ValidatedIssuerMetadata,
|
||||
validateOIDCIssuerWellKnown,
|
||||
validateWellKnownAuthentication,
|
||||
} from "./oidc/validate";
|
||||
import { OidcError } from "./oidc/error";
|
||||
import { SUPPORTED_MATRIX_VERSIONS } from "./version-support";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
@@ -65,26 +55,9 @@ interface AutoDiscoveryState {
|
||||
}
|
||||
interface WellKnownConfig extends Omit<IWellKnownConfig, "error">, AutoDiscoveryState {}
|
||||
|
||||
/**
|
||||
* @deprecated in favour of OidcClientConfig
|
||||
*/
|
||||
interface DelegatedAuthConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig, AutoDiscoveryState {}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export interface OidcClientConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig {
|
||||
metadata: ValidatedIssuerMetadata;
|
||||
signingKeys?: SigningKey[];
|
||||
}
|
||||
|
||||
export interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
|
||||
"m.homeserver": WellKnownConfig;
|
||||
"m.identity_server": WellKnownConfig;
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
"m.authentication"?: (OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,107 +291,10 @@ export class AutoDiscovery {
|
||||
}
|
||||
});
|
||||
|
||||
const authConfig = await this.discoverAndValidateAuthenticationConfig(wellknown);
|
||||
clientConfig[M_AUTHENTICATION.stable!] = authConfig;
|
||||
|
||||
// Step 8: Give the config to the caller (finally)
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate delegated auth configuration
|
||||
* @deprecated use discoverAndValidateAuthenticationConfig
|
||||
* - m.authentication config is present and valid
|
||||
* - delegated auth issuer openid-configuration is reachable
|
||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
||||
* When successful, DelegatedAuthConfig will be returned with endpoints used for delegated auth
|
||||
* Any errors are caught, and AutoDiscoveryState returned with error
|
||||
* @param wellKnown - configuration object as returned
|
||||
* by the .well-known auto-discovery endpoint
|
||||
* @returns Config or failure result
|
||||
*/
|
||||
public static async validateDiscoveryAuthenticationConfig(
|
||||
wellKnown: IClientWellKnown,
|
||||
): Promise<DelegatedAuthConfig | AutoDiscoveryState> {
|
||||
try {
|
||||
const authentication = M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown) || undefined;
|
||||
const homeserverAuthenticationConfig = validateWellKnownAuthentication(authentication);
|
||||
|
||||
const issuerOpenIdConfigUrl = `${this.sanitizeWellKnownUrl(
|
||||
homeserverAuthenticationConfig.issuer,
|
||||
)}/.well-known/openid-configuration`;
|
||||
const issuerWellKnown = await this.fetchWellKnownObject<unknown>(issuerOpenIdConfigUrl);
|
||||
|
||||
if (issuerWellKnown.action !== AutoDiscoveryAction.SUCCESS) {
|
||||
logger.error("Failed to fetch issuer openid configuration");
|
||||
throw new Error(OidcError.General);
|
||||
}
|
||||
|
||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown.raw);
|
||||
|
||||
const delegatedAuthConfig: DelegatedAuthConfig = {
|
||||
state: AutoDiscoveryAction.SUCCESS,
|
||||
error: null,
|
||||
...homeserverAuthenticationConfig,
|
||||
...validatedIssuerConfig,
|
||||
};
|
||||
return delegatedAuthConfig;
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message as unknown as OidcError;
|
||||
const errorType = Object.values(OidcError).includes(errorMessage) ? errorMessage : OidcError.General;
|
||||
|
||||
const state =
|
||||
errorType === OidcError.NotSupported ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR;
|
||||
|
||||
return {
|
||||
state,
|
||||
error: errorType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate delegated auth configuration
|
||||
* - m.authentication config is present and valid
|
||||
* - delegated auth issuer openid-configuration is reachable
|
||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
||||
* When successful, validated authentication metadata and optionally signing keys will be returned
|
||||
* Any errors are caught, and AutoDiscoveryState returned with error
|
||||
* @param wellKnown - configuration object as returned
|
||||
* by the .well-known auto-discovery endpoint
|
||||
* @returns Config or failure result
|
||||
*/
|
||||
public static async discoverAndValidateAuthenticationConfig(
|
||||
wellKnown: IClientWellKnown,
|
||||
): Promise<(OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState> {
|
||||
try {
|
||||
const authentication = M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown) || undefined;
|
||||
const result = await discoverAndValidateAuthenticationConfig(authentication);
|
||||
|
||||
// include this for backwards compatibility
|
||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(result.metadata);
|
||||
|
||||
const response = {
|
||||
state: AutoDiscoveryAction.SUCCESS,
|
||||
error: null,
|
||||
...validatedIssuerConfig,
|
||||
...result,
|
||||
};
|
||||
return response;
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message as unknown as OidcError;
|
||||
const errorType = Object.values(OidcError).includes(errorMessage) ? errorMessage : OidcError.General;
|
||||
|
||||
const state =
|
||||
errorType === OidcError.NotSupported ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR;
|
||||
|
||||
return {
|
||||
state,
|
||||
error: errorType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to automatically discover client configuration information
|
||||
* prior to logging in. Such information includes the homeserver URL
|
||||
|
||||
@@ -625,13 +625,10 @@ export interface IServerVersions {
|
||||
unstable_features: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export const M_AUTHENTICATION = new UnstableValue("m.authentication", "org.matrix.msc2965.authentication");
|
||||
|
||||
export interface IClientWellKnown {
|
||||
[key: string]: any;
|
||||
"m.homeserver"?: IWellKnownConfig;
|
||||
"m.identity_server"?: IWellKnownConfig;
|
||||
[M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965
|
||||
}
|
||||
|
||||
export interface IWellKnownConfig<T = IClientWellKnown> {
|
||||
@@ -645,14 +642,6 @@ export interface IWellKnownConfig<T = IClientWellKnown> {
|
||||
server_name?: string;
|
||||
}
|
||||
|
||||
export interface IDelegatedAuthConfig {
|
||||
// MSC2965
|
||||
/** The OIDC Provider/issuer the client should use */
|
||||
issuer: string;
|
||||
/** The optional URL of the web UI where the user can manage their account */
|
||||
account?: string;
|
||||
}
|
||||
|
||||
interface IKeyBackupPath {
|
||||
path: string;
|
||||
queryData?: {
|
||||
|
||||
@@ -151,3 +151,49 @@ export async function retryNetworkOperation<T>(maxAttempts: number, callback: ()
|
||||
}
|
||||
throw lastConnectionError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the backoff time for a request retry attempt.
|
||||
* This produces wait times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
|
||||
* failure was due to a rate limited request, the time specified in the error is returned.
|
||||
*
|
||||
* Returns -1 if the error is not retryable, or if we reach the maximum number of attempts.
|
||||
*
|
||||
* @param err - The error thrown by the http call
|
||||
* @param attempts - The number of attempts made so far, including the one that just failed.
|
||||
* @param retryConnectionError - Whether to retry on {@link ConnectionError} (CORS, connection is down, etc.)
|
||||
*/
|
||||
export function calculateRetryBackoff(err: any, attempts: number, retryConnectionError: boolean): number {
|
||||
if (attempts > 4) {
|
||||
return -1; // give up
|
||||
}
|
||||
|
||||
if (err instanceof ConnectionError && !retryConnectionError) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.httpStatus && (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401)) {
|
||||
// client error; no amount of retrying will save you now.
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.name === "AbortError") {
|
||||
// this is a client timeout, that is already very high 60s/80s
|
||||
// we don't want to retry, as it could do it for very long
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If we are trying to send an event (or similar) that is too large in any way, then retrying won't help
|
||||
if (err.name === "M_TOO_LARGE") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.name === "M_LIMIT_EXCEEDED") {
|
||||
const waitTime = err.data.retry_after_ms;
|
||||
if (waitTime > 0) {
|
||||
return waitTime;
|
||||
}
|
||||
}
|
||||
|
||||
return 1000 * Math.pow(2, attempts);
|
||||
}
|
||||
|
||||
@@ -198,8 +198,8 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
};
|
||||
|
||||
private async fetchRootEvent(): Promise<void> {
|
||||
this.rootEvent = this.room.findEventById(this.id);
|
||||
// If the rootEvent does not exist in the local stores, then fetch it from the server.
|
||||
// Always fetch the root event, even if we already have it, so we can get the latest
|
||||
// state (via unsigned).
|
||||
try {
|
||||
const eventData = await this.client.fetchRoomEvent(this.roomId, this.id);
|
||||
const mapper = this.client.getEventMapper();
|
||||
|
||||
@@ -16,7 +16,6 @@ limitations under the License.
|
||||
|
||||
import { IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts";
|
||||
|
||||
import { IDelegatedAuthConfig } from "../client";
|
||||
import { subtleCrypto, TextEncoder } from "../crypto/crypto";
|
||||
import { logger } from "../logger";
|
||||
import { randomString } from "../randomstring";
|
||||
@@ -209,7 +208,7 @@ export const completeAuthorizationCodeGrant = async (
|
||||
code: string,
|
||||
state: string,
|
||||
): Promise<{
|
||||
oidcClientSettings: IDelegatedAuthConfig & { clientId: string };
|
||||
oidcClientSettings: { clientId: string; issuer: string };
|
||||
tokenResponse: BearerTokenResponse;
|
||||
homeserverUrl: string;
|
||||
idTokenClaims: IdTokenClaims;
|
||||
|
||||
+17
-18
@@ -14,36 +14,35 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MetadataService, OidcClientSettingsStore, SigningKey } from "oidc-client-ts";
|
||||
import { MetadataService, OidcClientSettingsStore } from "oidc-client-ts";
|
||||
|
||||
import { IDelegatedAuthConfig } from "../client";
|
||||
import { isValidatedIssuerMetadata, ValidatedIssuerMetadata, validateWellKnownAuthentication } from "./validate";
|
||||
import { isValidatedIssuerMetadata, validateOIDCIssuerWellKnown } from "./validate";
|
||||
import { Method, timeoutSignal } from "../http-api";
|
||||
import { OidcClientConfig } from "./index";
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Discover and validate delegated auth configuration
|
||||
* - m.authentication config is present and valid
|
||||
* - delegated auth issuer openid-configuration is reachable
|
||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
||||
* Fetches https://oidc-issuer.example.com/.well-known/openid-configuration and other files linked therein.
|
||||
* When successful, validated metadata is returned
|
||||
* @param wellKnown - configuration object as returned
|
||||
* by the .well-known auto-discovery endpoint
|
||||
* @param issuer - the OIDC issuer as returned by the /auth_issuer API
|
||||
* @returns validated authentication metadata and optionally signing keys
|
||||
* @throws when delegated auth config is invalid or unreachable
|
||||
*/
|
||||
export const discoverAndValidateAuthenticationConfig = async (
|
||||
authenticationConfig?: IDelegatedAuthConfig,
|
||||
): Promise<
|
||||
IDelegatedAuthConfig & {
|
||||
metadata: ValidatedIssuerMetadata;
|
||||
signingKeys?: SigningKey[];
|
||||
}
|
||||
> => {
|
||||
const homeserverAuthenticationConfig = validateWellKnownAuthentication(authenticationConfig);
|
||||
export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Promise<OidcClientConfig> => {
|
||||
const issuerOpenIdConfigUrl = new URL(".well-known/openid-configuration", issuer);
|
||||
const issuerWellKnownResponse = await fetch(issuerOpenIdConfigUrl, {
|
||||
method: Method.Get,
|
||||
signal: timeoutSignal(5000),
|
||||
});
|
||||
const issuerWellKnown = await issuerWellKnownResponse.json();
|
||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown);
|
||||
|
||||
// create a temporary settings store so we can use metadata service for discovery
|
||||
// create a temporary settings store, so we can use metadata service for discovery
|
||||
const settings = new OidcClientSettingsStore({
|
||||
authority: homeserverAuthenticationConfig.issuer,
|
||||
authority: issuer,
|
||||
redirect_uri: "", // Not known yet, this is here to make the type checker happy
|
||||
client_id: "", // Not known yet, this is here to make the type checker happy
|
||||
});
|
||||
@@ -54,7 +53,7 @@ export const discoverAndValidateAuthenticationConfig = async (
|
||||
isValidatedIssuerMetadata(metadata);
|
||||
|
||||
return {
|
||||
...homeserverAuthenticationConfig,
|
||||
...validatedIssuerConfig,
|
||||
metadata,
|
||||
signingKeys,
|
||||
};
|
||||
|
||||
@@ -14,9 +14,21 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { SigningKey } from "oidc-client-ts";
|
||||
import { ValidatedIssuerConfig, ValidatedIssuerMetadata } from "./validate";
|
||||
|
||||
export * from "./authorize";
|
||||
export * from "./discovery";
|
||||
export * from "./error";
|
||||
export * from "./register";
|
||||
export * from "./tokenRefresher";
|
||||
export * from "./validate";
|
||||
|
||||
/**
|
||||
* Validated config for native OIDC authentication, as returned by {@link discoverAndValidateOIDCIssuerWellKnown}.
|
||||
* Contains metadata and signing keys from the issuer's well-known (https://oidc-issuer.example.com/.well-known/openid-configuration).
|
||||
*/
|
||||
export interface OidcClientConfig extends ValidatedIssuerConfig {
|
||||
metadata: ValidatedIssuerMetadata;
|
||||
signingKeys?: SigningKey[];
|
||||
}
|
||||
|
||||
@@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IDelegatedAuthConfig } from "../client";
|
||||
import { OidcClientConfig } from ".";
|
||||
import { OidcError } from "./error";
|
||||
import { Method } from "../http-api";
|
||||
import { logger } from "../logger";
|
||||
import { ValidatedIssuerConfig } from "./validate";
|
||||
import { NonEmptyArray } from "../@types/common";
|
||||
|
||||
/**
|
||||
@@ -112,13 +111,13 @@ const doRegistration = async (
|
||||
|
||||
/**
|
||||
* Attempts dynamic registration against the configured registration endpoint
|
||||
* @param delegatedAuthConfig - Auth config from ValidatedServerConfig
|
||||
* @param delegatedAuthConfig - Auth config from {@link discoverAndValidateOIDCIssuerWellKnown}
|
||||
* @param clientMetadata - The metadata for the client which to register
|
||||
* @returns Promise<string> resolved with registered clientId
|
||||
* @throws when registration is not supported, on failed request or invalid response
|
||||
*/
|
||||
export const registerOidcClient = async (
|
||||
delegatedAuthConfig: IDelegatedAuthConfig & ValidatedIssuerConfig,
|
||||
delegatedAuthConfig: OidcClientConfig,
|
||||
clientMetadata: OidcRegistrationClientMetadata,
|
||||
): Promise<string> => {
|
||||
if (!delegatedAuthConfig.registrationEndpoint) {
|
||||
|
||||
@@ -17,9 +17,8 @@ limitations under the License.
|
||||
import { IdTokenClaims, OidcClient, WebStorageStateStore } from "oidc-client-ts";
|
||||
|
||||
import { AccessTokens } from "../http-api";
|
||||
import { IDelegatedAuthConfig } from "../client";
|
||||
import { generateScope } from "./authorize";
|
||||
import { discoverAndValidateAuthenticationConfig } from "./discovery";
|
||||
import { discoverAndValidateOIDCIssuerWellKnown } from "./discovery";
|
||||
import { logger } from "../logger";
|
||||
|
||||
/**
|
||||
@@ -42,9 +41,9 @@ export class OidcTokenRefresher {
|
||||
|
||||
public constructor(
|
||||
/**
|
||||
* Delegated auth config as found in matrix client .well-known
|
||||
* The OIDC issuer as returned by the /auth_issuer API
|
||||
*/
|
||||
authConfig: IDelegatedAuthConfig,
|
||||
issuer: string,
|
||||
/**
|
||||
* id of this client as registered with the OP
|
||||
*/
|
||||
@@ -63,17 +62,17 @@ export class OidcTokenRefresher {
|
||||
*/
|
||||
private readonly idTokenClaims: IdTokenClaims,
|
||||
) {
|
||||
this.oidcClientReady = this.initialiseOidcClient(authConfig, clientId, deviceId, redirectUri);
|
||||
this.oidcClientReady = this.initialiseOidcClient(issuer, clientId, deviceId, redirectUri);
|
||||
}
|
||||
|
||||
private async initialiseOidcClient(
|
||||
authConfig: IDelegatedAuthConfig,
|
||||
issuer: string,
|
||||
clientId: string,
|
||||
deviceId: string,
|
||||
redirectUri: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const config = await discoverAndValidateAuthenticationConfig(authConfig);
|
||||
const config = await discoverAndValidateOIDCIssuerWellKnown(issuer);
|
||||
|
||||
const scope = generateScope(deviceId);
|
||||
|
||||
|
||||
+5
-27
@@ -17,7 +17,6 @@ limitations under the License.
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { OidcMetadata, SigninResponse } from "oidc-client-ts";
|
||||
|
||||
import { IDelegatedAuthConfig } from "../client";
|
||||
import { logger } from "../logger";
|
||||
import { OidcError } from "./error";
|
||||
|
||||
@@ -35,31 +34,6 @@ export type ValidatedIssuerConfig = {
|
||||
accountManagementActionsSupported?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates MSC2965 m.authentication config
|
||||
* Returns valid configuration
|
||||
* @param wellKnown - client well known as returned from ./well-known/client/matrix
|
||||
* @returns config - when present and valid
|
||||
* @throws when config is not found or invalid
|
||||
*/
|
||||
export const validateWellKnownAuthentication = (authentication?: IDelegatedAuthConfig): IDelegatedAuthConfig => {
|
||||
if (!authentication) {
|
||||
throw new Error(OidcError.NotSupported);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof authentication.issuer === "string" &&
|
||||
(!authentication.hasOwnProperty("account") || typeof authentication.account === "string")
|
||||
) {
|
||||
return {
|
||||
issuer: authentication.issuer,
|
||||
account: authentication.account,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(OidcError.Misconfigured);
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === "object" && !Array.isArray(value);
|
||||
const requiredStringProperty = (wellKnown: Record<string, unknown>, key: string): boolean => {
|
||||
@@ -150,7 +124,11 @@ export type ValidatedIssuerMetadata = Partial<OidcMetadata> &
|
||||
| "response_types_supported"
|
||||
| "grant_types_supported"
|
||||
| "code_challenge_methods_supported"
|
||||
>;
|
||||
> & {
|
||||
// MSC2965 extensions to the OIDC spec
|
||||
account_management_uri?: string;
|
||||
account_management_actions_supported?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps validateOIDCIssuerWellKnown in a type assertion
|
||||
|
||||
@@ -15,11 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
OlmMachine,
|
||||
KeysBackupRequest,
|
||||
KeysClaimRequest,
|
||||
KeysQueryRequest,
|
||||
KeysUploadRequest,
|
||||
OlmMachine,
|
||||
RoomMessageRequest,
|
||||
SignatureUploadRequest,
|
||||
ToDeviceRequest,
|
||||
@@ -27,8 +27,8 @@ import {
|
||||
} from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
|
||||
import { logDuration, QueryDict } from "../utils";
|
||||
import { calculateRetryBackoff, IHttpOpts, MatrixHttpApi, Method } from "../http-api";
|
||||
import { logDuration, QueryDict, sleep } from "../utils";
|
||||
import { IAuthDict, UIAuthCallback } from "../interactive-auth";
|
||||
import { UIAResponse } from "../@types/uia";
|
||||
import { ToDeviceMessageId } from "../@types/event";
|
||||
@@ -71,15 +71,15 @@ export class OutgoingRequestProcessor {
|
||||
* for the complete list of request types
|
||||
*/
|
||||
if (msg instanceof KeysUploadRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body);
|
||||
resp = await this.requestWithRetry(Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body);
|
||||
} else if (msg instanceof KeysQueryRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body);
|
||||
resp = await this.requestWithRetry(Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body);
|
||||
} else if (msg instanceof KeysClaimRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body);
|
||||
resp = await this.requestWithRetry(Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body);
|
||||
} else if (msg instanceof SignatureUploadRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body);
|
||||
resp = await this.requestWithRetry(Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body);
|
||||
} else if (msg instanceof KeysBackupRequest) {
|
||||
resp = await this.rawJsonRequest(
|
||||
resp = await this.requestWithRetry(
|
||||
Method.Put,
|
||||
"/_matrix/client/v3/room_keys/keys",
|
||||
{ version: msg.version },
|
||||
@@ -91,7 +91,7 @@ export class OutgoingRequestProcessor {
|
||||
const path =
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(msg.room_id)}/send/` +
|
||||
`${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`;
|
||||
resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
|
||||
resp = await this.requestWithRetry(Method.Put, path, {}, msg.body);
|
||||
} else if (msg instanceof UploadSigningKeysRequest) {
|
||||
await this.makeRequestWithUIA(
|
||||
Method.Post,
|
||||
@@ -154,7 +154,7 @@ export class OutgoingRequestProcessor {
|
||||
const path =
|
||||
`/_matrix/client/v3/sendToDevice/${encodeURIComponent(request.event_type)}/` +
|
||||
encodeURIComponent(request.txn_id);
|
||||
return await this.rawJsonRequest(Method.Put, path, {}, request.body);
|
||||
return await this.requestWithRetry(Method.Put, path, {}, request.body);
|
||||
}
|
||||
|
||||
private async makeRequestWithUIA<T>(
|
||||
@@ -165,7 +165,7 @@ export class OutgoingRequestProcessor {
|
||||
uiaCallback: UIAuthCallback<T> | undefined,
|
||||
): Promise<string> {
|
||||
if (!uiaCallback) {
|
||||
return await this.rawJsonRequest(method, path, queryParams, body);
|
||||
return await this.requestWithRetry(method, path, queryParams, body);
|
||||
}
|
||||
|
||||
const parsedBody = JSON.parse(body);
|
||||
@@ -176,7 +176,7 @@ export class OutgoingRequestProcessor {
|
||||
if (auth !== null) {
|
||||
newBody.auth = auth;
|
||||
}
|
||||
const resp = await this.rawJsonRequest(method, path, queryParams, JSON.stringify(newBody));
|
||||
const resp = await this.requestWithRetry(method, path, queryParams, JSON.stringify(newBody));
|
||||
return JSON.parse(resp) as T;
|
||||
};
|
||||
|
||||
@@ -184,6 +184,31 @@ export class OutgoingRequestProcessor {
|
||||
return JSON.stringify(resp);
|
||||
}
|
||||
|
||||
private async requestWithRetry(
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams: QueryDict,
|
||||
body: string,
|
||||
): Promise<string> {
|
||||
let currentRetryCount = 0;
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
return await this.rawJsonRequest(method, path, queryParams, body);
|
||||
} catch (e) {
|
||||
currentRetryCount++;
|
||||
const backoff = calculateRetryBackoff(e, currentRetryCount, true);
|
||||
if (backoff < 0) {
|
||||
// Max number of retries reached, or error is not retryable. rethrow the error
|
||||
throw e;
|
||||
}
|
||||
// wait for the specified time and then retry the request
|
||||
await sleep(backoff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async rawJsonRequest(method: Method, path: string, queryParams: QueryDict, body: string): Promise<string> {
|
||||
const opts = {
|
||||
// inhibit the JSON stringification and parsing within HttpApi.
|
||||
|
||||
+3
-28
@@ -22,7 +22,7 @@ import { logger } from "./logger";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { EventType } from "./@types/event";
|
||||
import { defer, IDeferred, removeElement } from "./utils";
|
||||
import { ConnectionError, MatrixError } from "./http-api";
|
||||
import { calculateRetryBackoff, MatrixError } from "./http-api";
|
||||
import { ISendEventResponse } from "./@types/requests";
|
||||
|
||||
const DEBUG = false; // set true to enable console logging.
|
||||
@@ -43,38 +43,13 @@ type ProcessFunction<T> = (event: MatrixEvent) => Promise<T>;
|
||||
// eslint-disable-next-line camelcase
|
||||
export class MatrixScheduler<T = ISendEventResponse> {
|
||||
/**
|
||||
* Retries events up to 4 times using exponential backoff. This produces wait
|
||||
* times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
|
||||
* failure was due to a rate limited request, the time specified in the error is
|
||||
* waited before being retried.
|
||||
* Default retry algorithm for the matrix scheduler. Retries events up to 4 times with exponential backoff.
|
||||
* @param attempts - Number of attempts that have been made, including the one that just failed (ie. starting at 1)
|
||||
* @see retryAlgorithm
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent | null, attempts: number, err: MatrixError): number {
|
||||
if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) {
|
||||
// client error; no amount of retrying with save you now.
|
||||
return -1;
|
||||
}
|
||||
if (err instanceof ConnectionError) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// if event that we are trying to send is too large in any way then retrying won't help
|
||||
if (err.name === "M_TOO_LARGE") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.name === "M_LIMIT_EXCEEDED") {
|
||||
const waitTime = err.data.retry_after_ms;
|
||||
if (waitTime > 0) {
|
||||
return waitTime;
|
||||
}
|
||||
}
|
||||
if (attempts > 4) {
|
||||
return -1; // give up
|
||||
}
|
||||
return 1000 * Math.pow(2, attempts);
|
||||
return calculateRetryBackoff(err, attempts, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -8,12 +8,12 @@
|
||||
"noUnusedLocals": true,
|
||||
"noEmit": true,
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./spec/**/*.ts"],
|
||||
"typedocOptions": {
|
||||
"entryPoints": ["src/matrix.ts"],
|
||||
"excludeExternals": true,
|
||||
"out": "_docs",
|
||||
},
|
||||
"out": "_docs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1299,10 +1299,10 @@
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "0.3.9"
|
||||
|
||||
"@es-joy/jsdoccomment@~0.41.0":
|
||||
version "0.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz#4a2f7db42209c0425c71a1476ef1bdb6dcd836f6"
|
||||
integrity sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==
|
||||
"@es-joy/jsdoccomment@~0.42.0":
|
||||
version "0.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.42.0.tgz#59e878708336aaee88c2b34c894f73dbf77ae2b0"
|
||||
integrity sha512-R1w57YlVA6+YE01wch3GPYn6bCsrOV3YW/5oGGE2tmX6JcL9Nr+b5IikrjMPF+v9CV3ay+obImEdsDhovhJrzw==
|
||||
dependencies:
|
||||
comment-parser "1.4.1"
|
||||
esquery "^1.5.0"
|
||||
@@ -1658,10 +1658,10 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@matrix-org/matrix-sdk-crypto-wasm@^4.5.0":
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.5.0.tgz#f55ae28f2cd1bce14fa98069840dd31b2172732a"
|
||||
integrity sha512-QINqNfBRrkVv9vB+hR+OW4RO6cwf8nt4GEbREiS09L5eUK6xa5REpeXyh7ty1QusYk/DBiNadNcM5ROEPk9dcQ==
|
||||
"@matrix-org/matrix-sdk-crypto-wasm@^4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.6.0.tgz#35224214c7638abbe2bc91fb4fa4fb022a1a2bf0"
|
||||
integrity sha512-v9PFWzSTWMlZKbyk3PPsZjUtOEQ7FIz5USD3lFRUWiS4pv0FOKR125VOUnR5Z/kAty57JXCHDAexCln3zE2Fww==
|
||||
|
||||
"@matrix-org/olm@3.2.15":
|
||||
version "3.2.15"
|
||||
@@ -1933,9 +1933,9 @@
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/node@18":
|
||||
version "18.19.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.14.tgz#1880ff1b3ac913f3877f711588e5ed227da01886"
|
||||
integrity sha512-EnQ4Us2rmOS64nHDWr0XqAD8DsO6f3XR6lf9UIIrZQpUzPVdN/oPuEzfDWNHSyXLvoGgjuEm/sPwFGSSs35Wtg==
|
||||
version "18.19.17"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.17.tgz#a581a9fb4b2cfdbc61f008804f4436b2d5c40354"
|
||||
integrity sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
@@ -1954,7 +1954,12 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/sdp-transform/-/sdp-transform-2.4.9.tgz#26ef39f487a6909b0512f580b80920a366b27f52"
|
||||
integrity sha512-bVr+/OoZZy7wrHlNcEAAa6PAgKA4BoXPYVN2EijMC5WnGgQ4ZEuixmKnVs2roiAvr7RhIFVH17QD27cojgIZCg==
|
||||
|
||||
"@types/semver@^7.3.12", "@types/semver@^7.5.0":
|
||||
"@types/semver@^7.3.12":
|
||||
version "7.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.7.tgz#326f5fdda70d13580777bcaa1bc6fa772a5aef0e"
|
||||
integrity sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==
|
||||
|
||||
"@types/semver@^7.5.0":
|
||||
version "7.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
|
||||
integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==
|
||||
@@ -1991,16 +1996,16 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^6.0.0":
|
||||
version "6.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.20.0.tgz#9cf31546d2d5e884602626d89b0e0d2168ac25ed"
|
||||
integrity sha512-fTwGQUnjhoYHeSF6m5pWNkzmDDdsKELYrOBxhjMrofPqCkoC2k3B2wvGHFxa1CTIqkEn88nlW1HVMztjo2K8Hg==
|
||||
"@typescript-eslint/eslint-plugin@^7.0.0":
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz#c13a34057be425167cc4a765158c46fdf2fd981d"
|
||||
integrity sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.5.1"
|
||||
"@typescript-eslint/scope-manager" "6.20.0"
|
||||
"@typescript-eslint/type-utils" "6.20.0"
|
||||
"@typescript-eslint/utils" "6.20.0"
|
||||
"@typescript-eslint/visitor-keys" "6.20.0"
|
||||
"@typescript-eslint/scope-manager" "7.0.2"
|
||||
"@typescript-eslint/type-utils" "7.0.2"
|
||||
"@typescript-eslint/utils" "7.0.2"
|
||||
"@typescript-eslint/visitor-keys" "7.0.2"
|
||||
debug "^4.3.4"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.2.4"
|
||||
@@ -2008,15 +2013,15 @@
|
||||
semver "^7.5.4"
|
||||
ts-api-utils "^1.0.1"
|
||||
|
||||
"@typescript-eslint/parser@^6.0.0":
|
||||
version "6.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.20.0.tgz#17e314177304bdf498527e3c4b112e41287b7416"
|
||||
integrity sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==
|
||||
"@typescript-eslint/parser@^7.0.0":
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.0.2.tgz#95c31233d343db1ca1df8df7811b5b87ca7b1a68"
|
||||
integrity sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "6.20.0"
|
||||
"@typescript-eslint/types" "6.20.0"
|
||||
"@typescript-eslint/typescript-estree" "6.20.0"
|
||||
"@typescript-eslint/visitor-keys" "6.20.0"
|
||||
"@typescript-eslint/scope-manager" "7.0.2"
|
||||
"@typescript-eslint/types" "7.0.2"
|
||||
"@typescript-eslint/typescript-estree" "7.0.2"
|
||||
"@typescript-eslint/visitor-keys" "7.0.2"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.62.0":
|
||||
@@ -2027,21 +2032,21 @@
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
"@typescript-eslint/visitor-keys" "5.62.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@6.20.0":
|
||||
version "6.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.20.0.tgz#8a926e60f6c47feb5bab878246dc2ae465730151"
|
||||
integrity sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==
|
||||
"@typescript-eslint/scope-manager@7.0.2":
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz#6ec4cc03752758ddd1fdaae6fbd0ed9a2ca4fe63"
|
||||
integrity sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "6.20.0"
|
||||
"@typescript-eslint/visitor-keys" "6.20.0"
|
||||
"@typescript-eslint/types" "7.0.2"
|
||||
"@typescript-eslint/visitor-keys" "7.0.2"
|
||||
|
||||
"@typescript-eslint/type-utils@6.20.0":
|
||||
version "6.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.20.0.tgz#d395475cd0f3610dd80c7d8716fa0db767da3831"
|
||||
integrity sha512-qnSobiJQb1F5JjN0YDRPHruQTrX7ICsmltXhkV536mp4idGAYrIyr47zF/JmkJtEcAVnIz4gUYJ7gOZa6SmN4g==
|
||||
"@typescript-eslint/type-utils@7.0.2":
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz#a7fc0adff0c202562721357e7478207d380a757b"
|
||||
integrity sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "6.20.0"
|
||||
"@typescript-eslint/utils" "6.20.0"
|
||||
"@typescript-eslint/typescript-estree" "7.0.2"
|
||||
"@typescript-eslint/utils" "7.0.2"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^1.0.1"
|
||||
|
||||
@@ -2050,10 +2055,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
|
||||
integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
|
||||
|
||||
"@typescript-eslint/types@6.20.0":
|
||||
version "6.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.20.0.tgz#5ccd74c29011ae7714ae6973e4ec0c634708b448"
|
||||
integrity sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==
|
||||
"@typescript-eslint/types@7.0.2":
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.0.2.tgz#b6edd108648028194eb213887d8d43ab5750351c"
|
||||
integrity sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.62.0":
|
||||
version "5.62.0"
|
||||
@@ -2068,13 +2073,13 @@
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@6.20.0":
|
||||
version "6.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.20.0.tgz#5b2d0975949e6bdd8d45ee1471461ef5fadc5542"
|
||||
integrity sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==
|
||||
"@typescript-eslint/typescript-estree@7.0.2":
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz#3c6dc8a3b9799f4ef7eca0d224ded01974e4cb39"
|
||||
integrity sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "6.20.0"
|
||||
"@typescript-eslint/visitor-keys" "6.20.0"
|
||||
"@typescript-eslint/types" "7.0.2"
|
||||
"@typescript-eslint/visitor-keys" "7.0.2"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
@@ -2082,17 +2087,17 @@
|
||||
semver "^7.5.4"
|
||||
ts-api-utils "^1.0.1"
|
||||
|
||||
"@typescript-eslint/utils@6.20.0":
|
||||
version "6.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.20.0.tgz#0e52afcfaa51af5656490ba4b7437cc3aa28633d"
|
||||
integrity sha512-/EKuw+kRu2vAqCoDwDCBtDRU6CTKbUmwwI7SH7AashZ+W+7o8eiyy6V2cdOqN49KsTcASWsC5QeghYuRDTyOOg==
|
||||
"@typescript-eslint/utils@7.0.2":
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.0.2.tgz#8756123054cd934c8ba7db6a6cffbc654b10b5c4"
|
||||
integrity sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@types/json-schema" "^7.0.12"
|
||||
"@types/semver" "^7.5.0"
|
||||
"@typescript-eslint/scope-manager" "6.20.0"
|
||||
"@typescript-eslint/types" "6.20.0"
|
||||
"@typescript-eslint/typescript-estree" "6.20.0"
|
||||
"@typescript-eslint/scope-manager" "7.0.2"
|
||||
"@typescript-eslint/types" "7.0.2"
|
||||
"@typescript-eslint/typescript-estree" "7.0.2"
|
||||
semver "^7.5.4"
|
||||
|
||||
"@typescript-eslint/utils@^5.10.0":
|
||||
@@ -2117,12 +2122,12 @@
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@6.20.0":
|
||||
version "6.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz#f7ada27f2803de89df0edd9fd7be22c05ce6a498"
|
||||
integrity sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==
|
||||
"@typescript-eslint/visitor-keys@7.0.2":
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz#2899b716053ad7094962beb895d11396fc12afc7"
|
||||
integrity sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "6.20.0"
|
||||
"@typescript-eslint/types" "7.0.2"
|
||||
eslint-visitor-keys "^3.4.1"
|
||||
|
||||
"@ungap/structured-clone@^1.2.0":
|
||||
@@ -3151,25 +3156,25 @@ eslint-plugin-import@^2.26.0:
|
||||
tsconfig-paths "^3.15.0"
|
||||
|
||||
eslint-plugin-jest@^27.1.6:
|
||||
version "27.6.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.6.3.tgz#8acb8b1e45597fe1f4d4cf25163d90119efc12be"
|
||||
integrity sha512-+YsJFVH6R+tOiO3gCJon5oqn4KWc+mDq2leudk8mrp8RFubLOo9CVyi3cib4L7XMpxExmkmBZQTPDYVBzgpgOA==
|
||||
version "27.9.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz#7c98a33605e1d8b8442ace092b60e9919730000b"
|
||||
integrity sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^5.10.0"
|
||||
|
||||
eslint-plugin-jsdoc@^48.0.0:
|
||||
version "48.0.4"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.0.4.tgz#ca54da8937b2f6fd242a16dbb1b9ff5050baf223"
|
||||
integrity sha512-A0cH+5svWPXzGZszBjXA1t0aAqVGS+/x3i02KFmb73rU0iMLnadEcVWcD/dGBZHIfAMKr3YpWh58f6wn4N909w==
|
||||
version "48.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.1.0.tgz#565363770b433485bfc70dc862b50b7f780529ec"
|
||||
integrity sha512-g9S8ukmTd1DVcV/xeBYPPXOZ6rc8WJ4yi0+MVxJ1jBOrz5kmxV9gJJQ64ltCqIWFnBChLIhLVx3tbTSarqVyFA==
|
||||
dependencies:
|
||||
"@es-joy/jsdoccomment" "~0.41.0"
|
||||
"@es-joy/jsdoccomment" "~0.42.0"
|
||||
are-docs-informative "^0.0.2"
|
||||
comment-parser "1.4.1"
|
||||
debug "^4.3.4"
|
||||
escape-string-regexp "^4.0.0"
|
||||
esquery "^1.5.0"
|
||||
is-builtin-module "^3.2.1"
|
||||
semver "^7.5.4"
|
||||
semver "^7.6.0"
|
||||
spdx-expression-parse "^4.0.0"
|
||||
|
||||
eslint-plugin-matrix-org@^1.0.0:
|
||||
@@ -3185,10 +3190,10 @@ eslint-plugin-tsdoc@^0.2.17:
|
||||
"@microsoft/tsdoc" "0.14.2"
|
||||
"@microsoft/tsdoc-config" "0.16.2"
|
||||
|
||||
eslint-plugin-unicorn@^50.0.0:
|
||||
version "50.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz#e539cdb02dfd893c603536264c4ed9505b70e3bf"
|
||||
integrity sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==
|
||||
eslint-plugin-unicorn@^51.0.0:
|
||||
version "51.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz#3641c5e110324c3739d6cb98fc1b99ada39f477b"
|
||||
integrity sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.22.20"
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
@@ -3435,9 +3440,9 @@ fast-levenshtein@^2.0.6:
|
||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.0.tgz#ca5e1a90b5e68f97fc8b61330d5819b82f5fab03"
|
||||
integrity sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
|
||||
integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
@@ -3838,10 +3843,10 @@ human-signals@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28"
|
||||
integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==
|
||||
|
||||
husky@^8.0.3:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
|
||||
integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==
|
||||
husky@^9.0.0:
|
||||
version "9.0.10"
|
||||
resolved "https://registry.yarnpkg.com/husky/-/husky-9.0.10.tgz#ddca8908deb5f244e9286865ebc80b54387672c2"
|
||||
integrity sha512-TQGNknoiy6bURzIO77pPRu+XHi6zI7T93rX+QnJsoYFf3xdjKOur+IlfqzJGMHIK/wXrLg+GsvMs8Op7vI2jVA==
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
version "0.6.3"
|
||||
@@ -4761,25 +4766,25 @@ lines-and-columns@^1.1.6:
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
lint-staged@^15.0.2:
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.0.tgz#3111534ca58096a3c8f70b044b6e7fe21b36f859"
|
||||
integrity sha512-TFZzUEV00f+2YLaVPWBWGAMq7So6yQx+GG8YRMDeOEIf95Zn5RyiLMsEiX4KTNl9vq/w+NqRJkLA1kPIo15ufQ==
|
||||
version "15.2.2"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.2.tgz#ad7cbb5b3ab70e043fa05bff82a09ed286bc4c5f"
|
||||
integrity sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==
|
||||
dependencies:
|
||||
chalk "5.3.0"
|
||||
commander "11.1.0"
|
||||
debug "4.3.4"
|
||||
execa "8.0.1"
|
||||
lilconfig "3.0.0"
|
||||
listr2 "8.0.0"
|
||||
listr2 "8.0.1"
|
||||
micromatch "4.0.5"
|
||||
pidtree "0.6.0"
|
||||
string-argv "0.3.2"
|
||||
yaml "2.3.4"
|
||||
|
||||
listr2@8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.0.0.tgz#aa7c230995f8ce378585f7c96c0c6d1cefa4700d"
|
||||
integrity sha512-u8cusxAcyqAiQ2RhYvV7kRKNLgUvtObIbhOX2NCXqvp1UU32xIg5CT22ykS2TPKJXZWJwtK3IKLiqAGlGNE+Zg==
|
||||
listr2@8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.0.1.tgz#4d3f50ae6cec3c62bdf0e94f5c2c9edebd4b9c34"
|
||||
integrity sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==
|
||||
dependencies:
|
||||
cli-truncate "^4.0.0"
|
||||
colorette "^2.0.20"
|
||||
@@ -5051,9 +5056,9 @@ npm-run-path@^4.0.1:
|
||||
path-key "^3.0.0"
|
||||
|
||||
npm-run-path@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00"
|
||||
integrity sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.2.0.tgz#224cdd22c755560253dd71b83a1ef2f758b2e955"
|
||||
integrity sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==
|
||||
dependencies:
|
||||
path-key "^4.0.0"
|
||||
|
||||
@@ -5319,10 +5324,10 @@ prelude-ls@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier@3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.4.tgz#4723cadeac2ce7c9227de758e5ff9b14e075f283"
|
||||
integrity sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==
|
||||
prettier@3.2.5:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368"
|
||||
integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==
|
||||
|
||||
pretty-format@^28.1.3:
|
||||
version "28.1.3"
|
||||
@@ -5570,9 +5575,9 @@ reusify@^1.0.4:
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rfdc@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
|
||||
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f"
|
||||
integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==
|
||||
|
||||
rimraf@^3.0.2:
|
||||
version "3.0.2"
|
||||
@@ -5646,7 +5651,14 @@ semver@^6.3.0, semver@^6.3.1:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||
|
||||
semver@^7.3.7, semver@^7.5.3, semver@^7.5.4:
|
||||
semver@^7.3.7, semver@^7.6.0:
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
|
||||
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^7.5.3, semver@^7.5.4:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
@@ -5781,9 +5793,9 @@ spdx-correct@^3.0.0:
|
||||
spdx-license-ids "^3.0.0"
|
||||
|
||||
spdx-exceptions@^2.1.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz#c07a4ede25b16e4f78e6707bbd84b15a45c19c1b"
|
||||
integrity sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66"
|
||||
integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==
|
||||
|
||||
spdx-expression-parse@^3.0.0:
|
||||
version "3.0.1"
|
||||
@@ -5802,9 +5814,9 @@ spdx-expression-parse@^4.0.0:
|
||||
spdx-license-ids "^3.0.0"
|
||||
|
||||
spdx-license-ids@^3.0.0:
|
||||
version "3.0.16"
|
||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f"
|
||||
integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==
|
||||
version "3.0.17"
|
||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz#887da8aa73218e51a1d917502d79863161a93f9c"
|
||||
integrity sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
@@ -5850,9 +5862,9 @@ string-width@^5.0.1, string-width@^5.1.2:
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
string-width@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.0.0.tgz#14aa1b7aaa126d5b64fa79d3c894da8a9650ba06"
|
||||
integrity sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw==
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.1.0.tgz#d994252935224729ea3719c49f7206dc9c46550a"
|
||||
integrity sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==
|
||||
dependencies:
|
||||
emoji-regex "^10.3.0"
|
||||
get-east-asian-width "^1.0.0"
|
||||
@@ -6154,20 +6166,20 @@ typed-array-length@^1.0.4:
|
||||
for-each "^0.3.3"
|
||||
is-typed-array "^1.1.9"
|
||||
|
||||
typedoc-plugin-coverage@^2.1.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-coverage/-/typedoc-plugin-coverage-2.2.0.tgz#760bb7bab606eed165e1e559df86003fa958ff3f"
|
||||
integrity sha512-/hq9nwSNBz2p7+VYfljT/zFSmaxN8tlfcIp6CCAaQN6VIxXCciYFIqR+pcckRhjmfHIeSJ5uy2OpCt5F683npA==
|
||||
typedoc-plugin-coverage@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-coverage/-/typedoc-plugin-coverage-3.1.0.tgz#2848956f76e02f927f4c84e85d7ea00dd0e941c9"
|
||||
integrity sha512-fxeJRrxQR3yM/aFZU7mOuatgRCztiMCbeNiCRVZKY6VNgOcVMC1HS+ZfZnlbLLteUOdeWleSQ6yntuipz5zi6A==
|
||||
|
||||
typedoc-plugin-mdn-links@^3.0.3:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-3.1.5.tgz#874e66ba5bdfb842b5c2377a6722c5f0777ceba2"
|
||||
integrity sha512-ZwV7fO3+S7KqbmMycBMYdeCyJ2eYVnoIc1CB64kc9NNwuZZuxocLFAqU3ZT+gihp22xNezrXciFas3ctsKLPgg==
|
||||
version "3.1.16"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-3.1.16.tgz#936408cff409b8f9b2b55d9f6589850c488a9858"
|
||||
integrity sha512-Jdnw3tI3KOOMMahUNiCwKXJcH+Ov9y3syRQ8HP1ce8nT2O9Bsg9aqqa0LWye+CdCiSpWJ/USzOK3MFNxRYzL4A==
|
||||
|
||||
typedoc-plugin-missing-exports@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-2.1.0.tgz#f283a407eb7b3a43dafc598f44bbfc2d36dc1704"
|
||||
integrity sha512-+1DhqZCEu7Vu5APnrqpPwl31D+hXpt1fV0Le9ycCRL1eLVdatdl6KVt4SEVwPxnEpKwgOn2dNX6I9+0F1aO2aA==
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-2.2.0.tgz#e39a04bab5b0d7f7b28507d64c07a4c40b788648"
|
||||
integrity sha512-2+XR1IcyQ5UwXZVJe9NE6HrLmNufT9i5OwoIuuj79VxuA3eYq+Y6itS9rnNV1D7UeQnUSH8kISYD73gHE5zw+w==
|
||||
|
||||
typedoc-plugin-versions-cli@^0.1.12:
|
||||
version "0.1.12"
|
||||
|
||||
Reference in New Issue
Block a user