Compare commits

...

24 Commits

Author SHA1 Message Date
RiotRobot 6b0d2a4ad3 v36.2.0-rc.0 2025-02-04 12:29:59 +00:00
Michael Telatynski 72519a0eb4 Make sonarcloud.yml more reusable (#4681)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-03 14:25:56 +00:00
Will Hunt 624b8a242e Fix topic types (#4678) 2025-02-03 09:03:58 +00:00
Michael Telatynski 5be104a35c Fix types around Terms (#4674) 2025-02-03 08:48:00 +00:00
Will Hunt c93128ed39 Handle empty m.room.topic (#4673)
* Define topic as optional.

* Change isProvided so that types work better.

* allow makeTopicContent and parseTopicContent to handle optional values for plain text

* linting

* Remove usage of optional

* Topic key may only contain legacy key.

* Add tests for other branches.
2025-02-03 08:13:44 +00:00
Hubert Chathi cc238c24ab Provide more options for starting dehydration (#4664)
* provide more options for starting dehydration

* improve doc comments and tests
2025-01-30 18:22:23 +00:00
RiotRobot 8175683d4b Merge branch 'master' into develop 2025-01-28 13:21:56 +00:00
Valere a2fd06bdf9 Add API to withdraw verification requirement CryptoAPI.withdrawVerificationRequirement (#4646)
* API to withdraw verification `CryptoAPi.withdrawVerificationRequirement`

* review: use set up function instead of beforeEach
2025-01-28 10:41:37 +00:00
Valere 7d8cbd6ef0 Device Dehydration | js-sdk: store/load dehydration key (#4599)
* feat(dehydrated): Use the dehydrated key cache API

* feat(dehydrated): Add signalling to device dehydration manager

* feat(dehydrated): fix unneeded call getCachedKey

* Upgrade to `matrix-sdk-crypto-wasm` v13.0.0

* review: quick fix and doc

* apply changes from review

* apply changes from review

* fix comment

* add some tests and emit an event on rehydration failure

* factor out event counter into a test util, since it may be useful elsewhere

* adjust test to cover a few more lines

* fix documentation

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* fix missing bracket

* add test for getting the dehydration key from SSSS

---------

Co-authored-by: Hubert Chathi <hubertc@matrix.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-01-27 22:05:23 +00:00
David Baker 44158bc843 Add unspecced backup disable flag (#4661)
* Add unspecced backup disable flag

* Add comment
2025-01-27 19:51:28 +00:00
Hugh Nimmo-Smith fd9a44e701 Revert "Distinguish room state and timeline events in embedded clients (#4574)" (#4656)
This reverts commit bdd4d82cb3.
2025-01-27 10:04:03 +00:00
Hubert Chathi df492800b4 Fix test that will break with matrix-sdk-crypto-wasm 13.0.0 (#4635)
* fix test that will break with matrix-sdk-crypto-wasm 13.0.0

* this test requires using a real timer
2025-01-24 16:47:45 +00:00
Michael Telatynski 161da05972 Tidy up some main exports (#4649)
Fix knip config

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-01-23 15:41:29 +00:00
Florian Duros ed397d99ed CryptoApi.resetEncryption should always create a new key backup (#4648)
* fix(crypto api): `resetEncryption` always calls `resetKeyBackup`

* test(crypto api): update `resetEncryption` tests

* chore(crypto api): add logging in `resetEncryption`
2025-01-23 15:35:37 +00:00
Michael Telatynski c0e30ceca0 Switch OIDC primarily to new /auth_metadata API (#4626) 2025-01-22 13:48:27 +00:00
Florian Duros 61375ef38a Add CryptoApi.resetEncryption (#4614)
* feat(crypto api): Add `CryptoApi#resetEncryption`

* docs(crypto api): Review changes

* test(crypto api): Cleaner way to handle key backup removal
2025-01-22 10:53:50 +00:00
renovate[bot] e5fda72884 Update dependency @stylistic/eslint-plugin to v2.13.0 (#4642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 16:05:16 +00:00
renovate[bot] acfc619f31 Update dependency @types/node to v18.19.71 (#4637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 15:47:02 +00:00
renovate[bot] 7a0af07bf7 Update all non-major dependencies (#4640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 15:37:01 +00:00
renovate[bot] ad1afbc45b Update guibranco/github-status-action-v2 digest to ecd54a0 (#4636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 14:56:15 +00:00
renovate[bot] b4ce4ce184 Update dependency @babel/eslint-parser to v7.26.5 (#4641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 14:55:12 +00:00
renovate[bot] ddbc66b905 Update typescript-eslint monorepo to v8.20.0 (#4643)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 14:53:48 +00:00
renovate[bot] ad1c67e33e Update dependency typescript to v5.7.3 (#4639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 14:53:09 +00:00
renovate[bot] d473e16df7 Update dependency typedoc-plugin-mdn-links to v4.0.8 (#4638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 14:53:05 +00:00
39 changed files with 1283 additions and 371 deletions
+3 -3
View File
@@ -27,7 +27,7 @@ jobs:
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
- uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61
- uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
@@ -70,7 +70,7 @@ jobs:
run: |
coverage=$(find coverage -type f -name '*lcov.info' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
echo "sonar.javascript.lcov.reportPaths=$coverage" >> sonar-project.properties
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
reports=$(find coverage -type f -name '*sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
echo "sonar.testExecutionReportPaths=$reports" >> sonar-project.properties
- name: "🩻 SonarCloud Scan"
@@ -87,7 +87,7 @@ jobs:
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
- uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61
- uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -116,7 +116,7 @@ jobs:
steps:
- name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false'
uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61
uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
+15 -3
View File
@@ -6,11 +6,21 @@ export default {
"src/types.ts",
"src/browser-index.ts",
"src/indexeddb-worker.ts",
"src/crypto-api/index.ts",
"src/testing.ts",
"src/matrix.ts",
"scripts/**",
"spec/**",
"release.sh",
// For now, we include all source files as entrypoints as we have been bad about gutwrenched imports
"src/**",
// XXX: these look entirely unused
"src/crypto/aes.ts",
"src/crypto/crypto.ts",
"src/crypto/recoverykey.ts",
// XXX: these should be re-exported by one of the supported exports
"src/matrixrtc/index.ts",
"src/sliding-sync.ts",
"src/webrtc/groupCall.ts",
"src/webrtc/stats/media/mediaTrackStats.ts",
"src/rendezvous/RendezvousChannel.ts",
],
project: ["**/*.{js,ts}"],
ignore: ["examples/**"],
@@ -33,4 +43,6 @@ export default {
"dist",
],
ignoreExportsUsedInFile: true,
includeEntryExports: false,
exclude: ["enumMembers"],
} satisfies KnipConfig;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "36.1.0",
"version": "36.2.0-rc.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=20.0.0"
@@ -50,7 +50,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^12.1.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^13.0.0",
"@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
+47 -1
View File
@@ -17,11 +17,13 @@ limitations under the License.
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src";
import { ClientEvent, createClient, MatrixClient, MatrixEvent } from "../../../src";
import { CryptoEvent } from "../../../src/crypto-api/index";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { emitPromise, EventCounter } from "../../test-utils/test-utils";
describe("Device dehydration", () => {
it("should rehydrate and dehydrate a device", async () => {
@@ -40,6 +42,12 @@ describe("Device dehydration", () => {
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
const creationEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydratedDeviceCreated);
const dehydrationKeyCachedEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydrationKeyCached);
const rehydrationStartedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationStarted);
const rehydrationCompletedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationCompleted);
const rehydrationProgressCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationProgress);
// count the number of times the dehydration key gets set
let setDehydrationCount = 0;
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
@@ -74,6 +82,8 @@ describe("Device dehydration", () => {
await crypto.startDehydration();
expect(dehydrationCount).toEqual(1);
expect(creationEventCounter.counter).toEqual(1);
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
// a week later, we should have created another dehydrated device
const dehydrationPromise = new Promise<void>((resolve, reject) => {
@@ -81,7 +91,10 @@ describe("Device dehydration", () => {
});
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await dehydrationPromise;
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
expect(dehydrationCount).toEqual(2);
expect(creationEventCounter.counter).toEqual(2);
// restart dehydration -- rehydrate the device that we created above,
// and create a new dehydrated device. We also set `createNewKey`, so
@@ -113,6 +126,39 @@ describe("Device dehydration", () => {
expect(setDehydrationCount).toEqual(2);
expect(eventsResponse.mock.calls).toHaveLength(2);
expect(rehydrationStartedCounter.counter).toEqual(1);
expect(rehydrationCompletedCounter.counter).toEqual(1);
expect(creationEventCounter.counter).toEqual(3);
expect(rehydrationProgressCounter.counter).toEqual(1);
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
// test that if we get an error when we try to rotate, it emits an event
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
},
});
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await rotationErrorEventPromise;
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
},
});
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
return {};
});
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
await crypto.startDehydration(true);
await rehydrationErrorEventPromise;
matrixClient.stopClient();
});
});
@@ -36,7 +36,7 @@ import {
MatrixError,
MatrixHttpApi,
} from "../../../src";
import { mockOpenIdConfiguration } from "../../test-utils/oidc";
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
const baseUrl = "https://example.com";
@@ -57,7 +57,7 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
getDomain: () => "example.com",
getDevice: jest.fn(),
getCrypto: jest.fn(() => crypto),
getAuthIssuer: jest.fn().mockResolvedValue({ issuer: "https://issuer/" }),
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
} as unknown as MatrixClient;
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
baseUrl: client.baseUrl,
@@ -69,10 +69,6 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
describe("MSC4108SignInWithQR", () => {
beforeEach(() => {
fetchMock.get(
"https://issuer/.well-known/openid-configuration",
mockOpenIdConfiguration("https://issuer/", [DEVICE_CODE_SCOPE]),
);
fetchMock.get("https://issuer/jwks", {
status: 200,
headers: {
+1 -39
View File
@@ -14,42 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
/**
* Makes a valid OidcClientConfig with minimum valid values
* @param issuer used as the base for all other urls
* @returns OidcClientConfig
*/
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
const metadata = mockOpenIdConfiguration(issuer);
return {
accountManagementEndpoint: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
metadata,
};
};
/**
* Useful for mocking <issuer>/.well-known/openid-configuration
* @param issuer used as the base for all other urls
* @returns ValidatedIssuerMetadata
*/
export const mockOpenIdConfiguration = (
issuer = "https://auth.org/",
additionalGrantTypes: string[] = [],
): ValidatedIssuerMetadata => ({
issuer,
revocation_endpoint: issuer + "revoke",
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
code_challenge_methods_supported: ["S256"],
});
export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../src/testing.ts";
+13
View File
@@ -566,6 +566,19 @@ if (globalThis.Olm) {
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
/**
* Counts the number of times that an event was emitted.
*/
export class EventCounter {
public counter;
constructor(emitter: EventEmitter, event: string) {
this.counter = 0;
emitter.on(event, () => {
this.counter++;
});
}
}
/**
* Advance the fake timers in a loop until the given promise resolves or rejects.
*
+32
View File
@@ -207,6 +207,17 @@ describe("Topic content helpers", () => {
],
});
});
it("creates an empty event when the topic is falsey", () => {
expect(makeTopicContent(undefined)).toEqual({
topic: undefined,
[M_TOPIC.name]: [],
});
expect(makeTopicContent(null)).toEqual({
topic: null,
[M_TOPIC.name]: [],
});
});
});
describe("parseTopicContent()", () => {
@@ -257,5 +268,26 @@ describe("Topic content helpers", () => {
html: "<b>pizza</b>",
});
});
it("parses legacy event content", () => {
expect(
parseTopicContent({
topic: "pizza",
}),
).toEqual({
text: "pizza",
});
});
it("uses legacy event content when new topic key is invalid", () => {
expect(
parseTopicContent({
"topic": "pizza",
"m.topic": {} as any,
}),
).toEqual({
text: "pizza",
});
});
});
});
+70 -7
View File
@@ -15,6 +15,7 @@ limitations under the License.
*/
import { Mocked, mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import { logger } from "../../src/logger";
import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
@@ -76,6 +77,7 @@ import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from ".
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { KnownMembership } from "../../src/@types/membership";
import { RoomMessageEventContent } from "../../src/@types/events";
import { mockOpenIdConfiguration } from "../test-utils/oidc.ts";
jest.useFakeTimers();
@@ -265,13 +267,17 @@ describe("MatrixClient", function () {
if (next.error) {
// eslint-disable-next-line
return Promise.reject({
errcode: (<MatrixError>next.error).errcode,
httpStatus: (<MatrixError>next.error).httpStatus,
name: (<MatrixError>next.error).errcode,
message: "Expected testing error",
data: next.error,
});
return Promise.reject(
new MatrixError(
{
errcode: (<MatrixError>next.error).errcode,
name: (<MatrixError>next.error).errcode,
message: "Expected testing error",
data: next.error,
},
(<MatrixError>next.error).httpStatus,
),
);
}
return Promise.resolve(next.data);
}
@@ -3489,6 +3495,63 @@ describe("MatrixClient", function () {
});
});
describe("getAuthMetadata", () => {
beforeEach(() => {
fetchMock.mockReset();
// This request is made by oidc-client-ts so is not intercepted by httpLookups
fetchMock.get("https://auth.org/jwks", {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
});
it("should use unstable prefix", async () => {
const metadata = mockOpenIdConfiguration();
httpLookups = [
{
method: "GET",
path: `/auth_metadata`,
data: metadata,
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
];
await expect(client.getAuthMetadata()).resolves.toEqual({
...metadata,
signingKeys: [],
});
expect(httpLookups.length).toEqual(0);
});
it("should fall back to auth_issuer + openid-configuration", async () => {
const metadata = mockOpenIdConfiguration();
httpLookups = [
{
method: "GET",
path: `/auth_metadata`,
error: new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404),
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
{
method: "GET",
path: `/auth_issuer`,
data: { issuer: metadata.issuer },
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
];
fetchMock.get("https://auth.org/.well-known/openid-configuration", metadata);
await expect(client.getAuthMetadata()).resolves.toEqual({
...metadata,
signingKeys: [],
});
expect(httpLookups.length).toEqual(0);
});
});
describe("identityHashedLookup", () => {
it("should return hashed lookup results", async () => {
const ID_ACCESS_TOKEN = "hello_id_server_please_let_me_make_a_request";
+5 -12
View File
@@ -40,8 +40,8 @@ jest.mock("jwt-decode");
describe("oidc authorization", () => {
const delegatedAuthConfig = makeDelegatedAuthConfig();
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
const authorizationEndpoint = delegatedAuthConfig.authorization_endpoint;
const tokenEndpoint = delegatedAuthConfig.token_endpoint;
const clientId = "xyz789";
const baseUrl = "https://test.com";
@@ -52,10 +52,7 @@ describe("oidc authorization", () => {
jest.spyOn(logger, "warn");
jest.setSystemTime(now);
fetchMock.get(
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
mockOpenIdConfiguration(),
);
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
globalThis.TextEncoder = TextEncoder;
});
@@ -127,11 +124,9 @@ describe("oidc authorization", () => {
it("should generate url with correct parameters", async () => {
const nonce = "abc123";
const metadata = delegatedAuthConfig.metadata;
const authUrl = new URL(
await generateOidcAuthorizationUrl({
metadata,
metadata: delegatedAuthConfig,
homeserverUrl: baseUrl,
clientId,
redirectUri: baseUrl,
@@ -156,11 +151,9 @@ describe("oidc authorization", () => {
it("should generate url with create prompt", async () => {
const nonce = "abc123";
const metadata = delegatedAuthConfig.metadata;
const authUrl = new URL(
await generateOidcAuthorizationUrl({
metadata,
metadata: delegatedAuthConfig,
homeserverUrl: baseUrl,
clientId,
redirectUri: baseUrl,
+6 -9
View File
@@ -42,13 +42,13 @@ describe("registerOidcClient()", () => {
});
it("should make correct request to register client", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 200,
body: JSON.stringify({ client_id: dynamicClientId }),
});
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
expect(fetchMockJest).toHaveBeenCalledWith(
delegatedAuthConfig.registrationEndpoint!,
delegatedAuthConfig.registration_endpoint,
expect.objectContaining({
headers: {
"Accept": "application/json",
@@ -72,7 +72,7 @@ describe("registerOidcClient()", () => {
});
it("should throw when registration request fails", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 500,
});
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
@@ -81,7 +81,7 @@ describe("registerOidcClient()", () => {
});
it("should throw when registration response is invalid", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 200,
// no clientId in response
body: "{}",
@@ -96,7 +96,7 @@ describe("registerOidcClient()", () => {
registerOidcClient(
{
...delegatedAuthConfig,
registrationEndpoint: undefined,
registration_endpoint: undefined,
},
metadata,
),
@@ -108,10 +108,7 @@ describe("registerOidcClient()", () => {
registerOidcClient(
{
...delegatedAuthConfig,
metadata: {
...delegatedAuthConfig.metadata,
grant_types_supported: [delegatedAuthConfig.metadata.grant_types_supported[0]],
},
grant_types_supported: [delegatedAuthConfig.grant_types_supported[0]],
},
metadata,
),
+11 -11
View File
@@ -55,8 +55,8 @@ describe("OidcTokenRefresher", () => {
});
beforeEach(() => {
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
fetchMock.get(`${config.issuer}.well-known/openid-configuration`, config);
fetchMock.get(`${config.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
@@ -64,7 +64,7 @@ describe("OidcTokenRefresher", () => {
keys: [],
});
fetchMock.post(config.tokenEndpoint, {
fetchMock.post(config.token_endpoint, {
status: 200,
headers: {
"Content-Type": "application/json",
@@ -81,7 +81,7 @@ describe("OidcTokenRefresher", () => {
it("throws when oidc client cannot be initialised", async () => {
jest.spyOn(logger, "error");
fetchMock.get(
`${config.metadata.issuer}.well-known/openid-configuration`,
`${config.issuer}.well-known/openid-configuration`,
{
ok: false,
status: 404,
@@ -126,7 +126,7 @@ describe("OidcTokenRefresher", () => {
const result = await refresher.doRefreshAccessToken("refresh-token");
expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
expect(fetchMock).toHaveFetched(config.token_endpoint, {
method: "POST",
});
@@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => {
it("should only have one inflight refresh request at once", async () => {
fetchMock
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 200,
headers: {
@@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 200,
headers: {
@@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => {
const result2 = await first;
// only one call to token endpoint
expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint);
expect(fetchMock).toHaveFetchedTimes(1, config.token_endpoint);
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.tokenEndpoint,
config.token_endpoint,
{
status: 503,
headers: {
@@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => {
// make sure inflight request is cleared after a failure
fetchMock
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 503,
headers: {
@@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 200,
headers: {
+31 -23
View File
@@ -18,17 +18,18 @@ import { mocked } from "jest-mock";
import { jwtDecode } from "jwt-decode";
import { logger } from "../../../src/logger";
import { validateIdToken, validateOIDCIssuerWellKnown } from "../../../src/oidc/validate";
import { ValidatedAuthMetadata, validateIdToken, validateAuthMetadata } from "../../../src/oidc/validate";
import { OidcError } from "../../../src/oidc/error";
jest.mock("jwt-decode");
describe("validateOIDCIssuerWellKnown", () => {
const validWk: any = {
const validWk: ValidatedAuthMetadata = {
issuer: "https://test.org",
authorization_endpoint: "https://test.org/authorize",
token_endpoint: "https://authorize.org/token",
registration_endpoint: "https://authorize.org/regsiter",
revocation_endpoint: "https://authorize.org/regsiter",
registration_endpoint: "https://authorize.org/register",
revocation_endpoint: "https://authorize.org/revoke",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
code_challenge_methods_supported: ["S256"],
@@ -44,14 +45,14 @@ describe("validateOIDCIssuerWellKnown", () => {
it("should throw OP support error when wellKnown is not an object", () => {
expect(() => {
validateOIDCIssuerWellKnown([]);
validateAuthMetadata([]);
}).toThrow(OidcError.OpSupport);
expect(logger.error).toHaveBeenCalledWith("Issuer configuration not found or malformed");
});
it("should log all errors before throwing", () => {
expect(() => {
validateOIDCIssuerWellKnown({
validateAuthMetadata({
...validWk,
authorization_endpoint: undefined,
response_types_supported: [],
@@ -62,24 +63,31 @@ describe("validateOIDCIssuerWellKnown", () => {
});
it("should return validated issuer config", () => {
expect(validateOIDCIssuerWellKnown(validWk)).toEqual({
authorizationEndpoint: validWk.authorization_endpoint,
tokenEndpoint: validWk.token_endpoint,
registrationEndpoint: validWk.registration_endpoint,
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
accountManagementEndpoint: "https://authorize.org/account",
});
expect(validateAuthMetadata(validWk)).toEqual(
expect.objectContaining({
issuer: validWk.issuer,
authorization_endpoint: validWk.authorization_endpoint,
token_endpoint: validWk.token_endpoint,
registration_endpoint: validWk.registration_endpoint,
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
account_management_uri: "https://authorize.org/account",
}),
);
});
it("should return validated issuer config without registrationendpoint", () => {
const wk = { ...validWk };
delete wk.registration_endpoint;
expect(validateOIDCIssuerWellKnown(wk)).toEqual({
authorizationEndpoint: validWk.authorization_endpoint,
tokenEndpoint: validWk.token_endpoint,
registrationEndpoint: undefined,
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
accountManagementEndpoint: "https://authorize.org/account",
it("should return validated issuer config without registration_endpoint", () => {
const { registration_endpoint: _, ...wk } = validWk;
expect(validateAuthMetadata(wk)).toEqual({
issuer: validWk.issuer,
authorization_endpoint: validWk.authorization_endpoint,
token_endpoint: validWk.token_endpoint,
revocation_endpoint: validWk.revocation_endpoint,
registration_endpoint: undefined,
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
account_management_uri: "https://authorize.org/account",
code_challenge_methods_supported: ["S256"],
grant_types_supported: ["authorization_code"],
response_types_supported: ["code"],
});
});
@@ -106,7 +114,7 @@ describe("validateOIDCIssuerWellKnown", () => {
...validWk,
[key]: value,
};
expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcError.OpSupport);
expect(() => validateAuthMetadata(wk)).toThrow(OidcError.OpSupport);
});
});
+424 -13
View File
@@ -44,7 +44,7 @@ import {
MemoryCryptoStore,
TypedEventEmitter,
} from "../../../src";
import { mkEvent } from "../../test-utils/test-utils";
import { emitPromise, mkEvent } from "../../test-utils/test-utils";
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
import { IEventDecryptionResult, IMegolmSessionData } from "../../../src/@types/crypto";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
@@ -61,9 +61,12 @@ import {
EventShieldReason,
ImportRoomKeysOpts,
KeyBackupCheck,
KeyBackupInfo,
VerificationRequest,
} from "../../../src/crypto-api";
import * as testData from "../../test-utils/test-data";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { defer } from "../../../src/utils";
import { logger } from "../../../src/logger";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
@@ -72,6 +75,7 @@ import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
import { RustBackupManager } from "../../../src/rust-crypto/backup.ts";
const TEST_USER = "@alice:example.com";
const TEST_DEVICE_ID = "TEST_DEVICE";
@@ -1532,13 +1536,58 @@ describe("RustCrypto", () => {
describe("pinCurrentIdentity", () => {
let rustCrypto: RustCrypto;
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
beforeEach(() => {
olmMachine = {
beforeEach(async () => {
const secretStorageCallbacks = {
getSecretStorageKey: async (keys: any, name: string) => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
} as SecretStorageCallbacks;
const secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
rustCrypto = await makeTestRustCrypto(
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
}),
TEST_USER,
TEST_DEVICE_ID,
secretStorage,
);
});
it("throws an error for an unknown user", async () => {
await expect(rustCrypto.pinCurrentUserIdentity("@other_user:example.com")).rejects.toThrow(
"Cannot pin identity of unknown user",
);
});
it("throws an error for our own user", async () => {
jest.useRealTimers();
const e2eKeyReceiver = new E2EKeyReceiver("http://server");
const e2eKeyResponder = new E2EKeyResponder("http://server");
e2eKeyResponder.addKeyReceiver(TEST_USER, e2eKeyReceiver);
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {
status: 200,
body: {},
});
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {
status: 200,
body: {},
});
await rustCrypto.bootstrapCrossSigning({ setupNewCrossSigning: true });
await expect(rustCrypto.pinCurrentUserIdentity(TEST_USER)).rejects.toThrow(
"Cannot pin identity of own user",
);
});
});
describe("withdraw verification", () => {
function createTestSetup(): { olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>; rustCrypto: RustCrypto } {
const olmMachine = {
getIdentity: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(
const rustCrypto = new RustCrypto(
logger,
olmMachine,
{} as MatrixClient["http"],
@@ -1547,21 +1596,40 @@ describe("RustCrypto", () => {
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
});
return { olmMachine, rustCrypto };
}
it("throws an error for an unknown user", async () => {
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of unknown user",
const { rustCrypto } = createTestSetup();
await expect(rustCrypto.withdrawVerificationRequirement("@alice:example.com")).rejects.toThrow(
"Cannot withdraw verification of unknown user",
);
});
it("throws an error for our own user", async () => {
const ownIdentity = new RustSdkCryptoJs.OwnUserIdentity();
it("Calls withdraw for other identity", async () => {
const { olmMachine, rustCrypto } = createTestSetup();
const identity = {
withdrawVerification: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OtherUserIdentity>;
olmMachine.getIdentity.mockResolvedValue(identity);
await rustCrypto.withdrawVerificationRequirement("@bob:example.com");
expect(identity.withdrawVerification).toHaveBeenCalled();
});
it("Calls withdraw for own identity", async () => {
const { olmMachine, rustCrypto } = createTestSetup();
const ownIdentity = {
withdrawVerification: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OwnUserIdentity>;
olmMachine.getIdentity.mockResolvedValue(ownIdentity);
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of own user",
);
await rustCrypto.withdrawVerificationRequirement("@alice:example.com");
expect(ownIdentity.withdrawVerification).toHaveBeenCalled();
});
});
@@ -1722,6 +1790,297 @@ describe("RustCrypto", () => {
});
expect(await rustCrypto.isDehydrationSupported()).toBe(true);
});
it("should load the dehydration key from SSSS if available", async () => {
fetchMock.config.overwriteRoutes = true;
const secretStorageCallbacks = {
getSecretStorageKey: async (keys: any, name: string) => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
} as SecretStorageCallbacks;
const secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
// Create a RustCrypto to set up device dehydration.
const e2eKeyReceiver1 = new E2EKeyReceiver("http://server");
const e2eKeyResponder1 = new E2EKeyResponder("http://server");
e2eKeyResponder1.addKeyReceiver(TEST_USER, e2eKeyReceiver1);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {
status: 200,
body: {},
});
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {
status: 200,
body: {},
});
const rustCrypto1 = await makeTestRustCrypto(makeMatrixHttpApi(), TEST_USER, TEST_DEVICE_ID, secretStorage);
// dehydration requires secret storage and cross signing
async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
await rustCrypto1.bootstrapCrossSigning({ setupNewCrossSigning: true });
await rustCrypto1.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
// we need to process a sync so that the OlmMachine will upload keys
await rustCrypto1.preprocessToDeviceMessages([]);
await rustCrypto1.onSyncCompleted({});
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
let dehydratedDeviceBody: any;
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
dehydratedDeviceBody = JSON.parse(opts.body as string);
return {};
});
await rustCrypto1.startDehydration();
await rustCrypto1.stop();
// Create another RustCrypto, using the same SecretStorage, to
// rehydrate the device.
const e2eKeyReceiver2 = new E2EKeyReceiver("http://server");
const e2eKeyResponder2 = new E2EKeyResponder("http://server");
e2eKeyResponder2.addKeyReceiver(TEST_USER, e2eKeyReceiver2);
const rustCrypto2 = await makeTestRustCrypto(
makeMatrixHttpApi(),
TEST_USER,
"ANOTHERDEVICE",
secretStorage,
);
// dehydration requires secret storage and cross signing
await rustCrypto2.bootstrapCrossSigning({ setupNewCrossSigning: true });
// we need to process a sync so that the OlmMachine will upload keys
await rustCrypto2.preprocessToDeviceMessages([]);
await rustCrypto2.onSyncCompleted({});
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
device_id: dehydratedDeviceBody.device_id,
device_data: dehydratedDeviceBody.device_data,
});
fetchMock.post(
`path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/${encodeURIComponent(dehydratedDeviceBody.device_id)}/events`,
{
events: [],
next_batch: "token",
},
);
// We check that a RehydrationCompleted event gets emitted, which
// means that the device was successfully rehydrated.
const rehydrationCompletedPromise = emitPromise(rustCrypto2, CryptoEvent.RehydrationCompleted);
await rustCrypto2.startDehydration();
await rehydrationCompletedPromise;
await rustCrypto2.stop();
});
describe("start dehydration options", () => {
let rustCrypto: RustCrypto;
let secretStorage: ServerSideSecretStorageImpl;
let dehydratedDeviceInfo: Record<string, any> | undefined;
// Function that is called when `GET /dehydrated_device` is called
// (i.e. when we try to rehydrate a device)
const getDehydratedDeviceMock = jest.fn(() => {
if (dehydratedDeviceInfo) {
return {
status: 200,
body: dehydratedDeviceInfo,
};
} else {
return {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
};
}
});
// Function that is called when `PUT /dehydrated_device` is called
// (i.e. when we create a new dehydrated device)
const putDehydratedDeviceMock = jest.fn((path, opts) => {
const content = JSON.parse(opts.body as string);
dehydratedDeviceInfo = {
device_id: content.device_id,
device_data: content.device_data,
};
return {
status: 200,
body: {
device_id: content.device_id,
},
};
});
beforeEach(async () => {
// Set up a RustCrypto object with secret storage and cross-signing.
const secretStorageCallbacks = {
getSecretStorageKey: async (keys: any, name: string) => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
} as SecretStorageCallbacks;
secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
const e2eKeyReceiver = new E2EKeyReceiver("http://server");
const e2eKeyResponder = new E2EKeyResponder("http://server");
e2eKeyResponder.addKeyReceiver(TEST_USER, e2eKeyReceiver);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {
status: 200,
body: {},
});
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {
status: 200,
body: {},
});
rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), TEST_USER, TEST_DEVICE_ID, secretStorage);
// dehydration requires secret storage and cross signing
async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
await rustCrypto.bootstrapCrossSigning({ setupNewCrossSigning: true });
await rustCrypto.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
// we need to process a sync so that the OlmMachine will upload keys
await rustCrypto.preprocessToDeviceMessages([]);
await rustCrypto.onSyncCompleted({});
// set up mocks needed for device dehydration
dehydratedDeviceInfo = undefined;
fetchMock.get(
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
getDehydratedDeviceMock,
);
fetchMock.put(
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
putDehydratedDeviceMock,
);
fetchMock.post(/_matrix\/client\/unstable\/org.matrix.msc3814.v1\/dehydrated_device\/.*\/events/, {
status: 200,
body: {
events: [],
next_batch: "foo",
},
});
getDehydratedDeviceMock.mockClear();
putDehydratedDeviceMock.mockClear();
});
afterEach(() => {
rustCrypto.stop();
});
// Several tests require a dehydrated device and dehydration key
// already set up.
async function setUpInitialDehydratedDevice() {
await rustCrypto.startDehydration();
getDehydratedDeviceMock.mockClear();
putDehydratedDeviceMock.mockClear();
return await secretStorage.get("org.matrix.msc3814");
}
it("should create a new key and dehydrate a device when no options given", async () => {
// With the default options, when we don't have an existing key ...
await rustCrypto.startDehydration();
// ... we create a new dehydration key ...
expect(await secretStorage.get("org.matrix.msc3814")).toBeTruthy();
// ... and create a new dehydrated device.
expect(putDehydratedDeviceMock).toHaveBeenCalled();
});
it("should rehydrate a device if available and keep existing key when no options given", async () => {
const origDehydrationKey = await setUpInitialDehydratedDevice();
// If we already have a dehydration key and dehydrated device...
await rustCrypto.startDehydration();
// ... we should fetch the device to rehydrate it ...
expect(getDehydratedDeviceMock).toHaveBeenCalled();
// ... create a new dehydrated device ...
expect(putDehydratedDeviceMock).toHaveBeenCalled();
// ... and keep the same dehydration key.
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
});
it("should do nothing if onlyIfKeyCached is true and we have no key cached", async () => {
// Since there is no key cached, so should do nothing. i.e. it
// should not make any HTTP requests and should not create a new key.
await rustCrypto.startDehydration({ onlyIfKeyCached: true });
expect(getDehydratedDeviceMock).not.toHaveBeenCalled();
expect(putDehydratedDeviceMock).not.toHaveBeenCalled();
expect(await secretStorage.get("org.matrix.msc3814")).toBeFalsy();
});
it("should start dehydration when onlyIfKeyCached is true, and we have a cached key", async () => {
const origDehydrationKey = await setUpInitialDehydratedDevice();
// If `onlyIfKeyCached` is `true`, and we already have have a
// key, we should behave the same as if no options were given.
await rustCrypto.startDehydration({ onlyIfKeyCached: true });
expect(getDehydratedDeviceMock).toHaveBeenCalled();
expect(putDehydratedDeviceMock).toHaveBeenCalled();
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
});
it("should not rehydrate if rehydrate is set to false", async () => {
const origDehydrationKey = await setUpInitialDehydratedDevice();
// If `rehydrate` is set to `false` ...
await rustCrypto.startDehydration({ rehydrate: false });
// ... we should not try to rehydrate ...
expect(getDehydratedDeviceMock).not.toHaveBeenCalled();
// ... but we should still create a new dehydrated device ...
expect(putDehydratedDeviceMock).toHaveBeenCalled();
// ... and we should keep the same dehydration key.
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
});
it("should create a new key if createNewKey is set to true", async () => {
const origDehydrationKey = await setUpInitialDehydratedDevice();
// If `createNewKey` is set to `true` ...
await rustCrypto.startDehydration({ createNewKey: true });
// ... we should rehydrate and dehydrate as normal ...
expect(getDehydratedDeviceMock).toHaveBeenCalled();
expect(putDehydratedDeviceMock).toHaveBeenCalled();
// ... and we should create a new dehydration key.
expect(await secretStorage.get("org.matrix.msc3814")).not.toEqual(origDehydrationKey);
});
});
});
describe("import & export secrets bundle", () => {
@@ -1879,6 +2238,58 @@ describe("RustCrypto", () => {
);
});
});
describe("resetEncryption", () => {
let secretStorage: ServerSideSecretStorage;
beforeEach(() => {
secretStorage = {
setDefaultKeyId: jest.fn(),
hasKey: jest.fn().mockResolvedValue(false),
getKey: jest.fn().mockResolvedValue(null),
} as unknown as ServerSideSecretStorage;
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
});
it("reset should reset 4S, backup and cross-signing", async () => {
// When we will delete the key backup
let backupIsDeleted = false;
fetchMock.delete("path:/_matrix/client/v3/room_keys/version/1", () => {
backupIsDeleted = true;
return {};
});
// If the backup is deleted, we will return an empty object
fetchMock.get("path:/_matrix/client/v3/room_keys/version", () => {
return backupIsDeleted ? {} : testData.SIGNED_BACKUP_DATA;
});
// A new key backup should be created after the reset
let newKeyBackupInfo!: KeyBackupInfo;
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (res, options) => {
newKeyBackupInfo = JSON.parse(options.body as string);
return { version: "2" };
});
// We consider the key backup as trusted
jest.spyOn(RustBackupManager.prototype, "isKeyBackupTrusted").mockResolvedValue({
trusted: true,
matchesDecryptionKey: true,
});
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
// We have a key backup
expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull();
const authUploadDeviceSigningKeys = jest.fn();
await rustCrypto.resetEncryption(authUploadDeviceSigningKeys);
// A new key backup should be created
expect(newKeyBackupInfo.auth_data).toBeTruthy();
// The new cross signing keys should be uploaded
expect(authUploadDeviceSigningKeys).toHaveBeenCalledWith(expect.any(Function));
});
});
});
/** Build a MatrixHttpApi instance */
+3
View File
@@ -374,6 +374,9 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents {
[EventType.Direct]: { [userId: string]: string[] };
[EventType.IgnoredUserList]: { [userId: string]: {} };
"m.secret_storage.default_key": { key: string };
// Flag set by the rust SDK (Element X) and also used by us to mark that the user opted out of backup
// (I don't know why it's m.org.matrix...)
"m.org.matrix.custom.backup_disabled": { disabled: boolean };
"m.identity_server": { base_url: string | null };
[key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings;
[key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription;
+36
View File
@@ -65,3 +65,39 @@ export interface IUsageLimit {
// eslint-disable-next-line camelcase
admin_contact?: string;
}
/**
* A policy name & url in a specific internationalisation
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms_response-200_internationalised-policy
*/
export interface InternationalisedPolicy {
name: string;
url: string;
}
/**
* A versioned policy with internationalised variants
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms_response-200_policy-object
*/
export interface Policy {
/**
* The version for the policy.
* There are no requirements on what this might be and could be alpha, semantically versioned, or arbitrary.
*/
version: string;
/**
* The policy information for the specified language.
* @remarks the type has to include a union with string due to limitations in the type system.
*/
[lang: string]: InternationalisedPolicy | string;
}
/**
* Response from the Terms API for Identity servers
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms
*/
export interface Terms {
policies: {
[policyName: string]: Policy;
};
}
+1 -1
View File
@@ -90,7 +90,7 @@ export interface RoomNameEventContent {
}
export interface RoomTopicEventContent {
topic: string;
topic: string | undefined | null;
}
export interface RoomAvatarEventContent {
+7 -5
View File
@@ -13,9 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EitherAnd } from "matrix-events-sdk";
import { UnstableValue } from "../NamespacedValue.ts";
import { IMessageRendering } from "./extensible_events.ts";
@@ -52,12 +49,17 @@ export const M_TOPIC = new UnstableValue("m.topic", "org.matrix.msc3765.topic");
*/
export type MTopicContent = IMessageRendering[];
type MTopicStable = { [M_TOPIC.altName]: MTopicContent };
type MTopicUnstable = { [M_TOPIC.name]: MTopicContent };
/**
* The event definition for an m.topic event (in content)
*/
export type MTopicEvent = EitherAnd<{ [M_TOPIC.name]: MTopicContent }, { [M_TOPIC.altName]: MTopicContent }>;
export type MTopicEvent = (MTopicStable & MTopicUnstable) | MTopicStable | MTopicUnstable;
/**
* The event content for an m.room.topic event
*/
export type MRoomTopicEventContent = { topic: string } & MTopicEvent;
export type MRoomTopicEventContent = {
topic: string | null | undefined;
} & Partial<MTopicEvent>;
+43 -3
View File
@@ -157,6 +157,7 @@ import {
IdServerUnbindResult,
JoinRule,
Preset,
Terms,
Visibility,
} from "./@types/partials.ts";
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper.ts";
@@ -247,6 +248,7 @@ import { ImageInfo } from "./@types/media.ts";
import { Capabilities, ServerCapabilities } from "./serverCapabilities.ts";
import { sha256 } from "./digest.ts";
import { keyFromAuthData } from "./common-crypto/key-passphrase.ts";
import { discoverAndValidateOIDCIssuerWellKnown, OidcClientConfig, validateAuthMetadataAndKeys } from "./oidc/index.ts";
export type Store = IStore;
@@ -2310,6 +2312,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
CryptoEvent.KeysChanged,
CryptoEvent.DevicesUpdated,
CryptoEvent.WillUpdateDevices,
CryptoEvent.DehydratedDeviceCreated,
CryptoEvent.DehydratedDeviceUploaded,
CryptoEvent.RehydrationStarted,
CryptoEvent.RehydrationProgress,
CryptoEvent.RehydrationCompleted,
CryptoEvent.RehydrationError,
CryptoEvent.DehydrationKeyCached,
CryptoEvent.DehydratedDeviceRotationError,
]);
}
@@ -4481,11 +4491,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
/**
* @param roomId - The room to update the topic in.
* @param topic - The plaintext topic. May be empty to remove the topic.
* @param htmlTopic - Optional.
* @returns Promise which resolves: TODO
* @returns Rejects: with an error response.
*/
public setRoomTopic(roomId: string, topic: string, htmlTopic?: string): Promise<ISendEventResponse> {
public setRoomTopic(roomId: string, topic?: string, htmlTopic?: string): Promise<ISendEventResponse> {
const content = ContentHelpers.makeTopicContent(topic, htmlTopic);
return this.sendStateEvent(roomId, EventType.RoomTopic, content);
}
@@ -10025,8 +10037,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(Method.Get, path, params);
}
public getTerms(serviceType: SERVICE_TYPES, baseUrl: string): Promise<any> {
// TODO: Types
public getTerms(serviceType: SERVICE_TYPES, baseUrl: string): Promise<Terms> {
const url = this.termsUrlForService(serviceType, baseUrl);
return this.http.requestOtherUrl(Method.Get, url);
}
@@ -10352,6 +10363,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Resolves: A promise of an object containing the OIDC issuer if configured
* @returns Rejects: when the request fails (module:http-api.MatrixError)
* @experimental - part of MSC2965
* @deprecated in favour of getAuthMetadata
*/
public async getAuthIssuer(): Promise<{
issuer: string;
@@ -10360,6 +10372,34 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
});
}
/**
* Discover and validate delegated auth configuration
* - delegated auth issuer openid-configuration is reachable
* - delegated auth issuer openid-configuration is configured correctly for us
* Fetches /auth_metadata falling back to legacy implementation using /auth_issuer followed by
* https://oidc-issuer.example.com/.well-known/openid-configuration and other files linked therein.
* When successful, validated metadata is returned
* @returns validated authentication metadata and optionally signing keys
* @throws when delegated auth config is invalid or unreachable
* @experimental - part of MSC2965
*/
public async getAuthMetadata(): Promise<OidcClientConfig> {
let authMetadata: unknown | undefined;
try {
authMetadata = await this.http.request<unknown>(Method.Get, "/auth_metadata", undefined, undefined, {
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
});
} catch (e) {
if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") {
const { issuer } = await this.getAuthIssuer();
return discoverAndValidateOIDCIssuerWellKnown(issuer);
}
throw e;
}
return validateAuthMetadataAndKeys(authMetadata);
}
}
function getUnstableDelayQueryOpts(delayOpts: SendDelayedEventRequestOpts): QueryDict {
+10 -6
View File
@@ -185,27 +185,31 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
/**
* Topic event helpers
*/
export type MakeTopicContent = (topic: string, htmlTopic?: string) => MRoomTopicEventContent;
export type MakeTopicContent = (topic: string | null | undefined, htmlTopic?: string) => MRoomTopicEventContent;
export const makeTopicContent: MakeTopicContent = (topic, htmlTopic) => {
const renderings = [{ body: topic, mimetype: "text/plain" }];
const renderings = [];
if (isProvided(topic)) {
renderings.push({ body: topic, mimetype: "text/plain" });
}
if (isProvided(htmlTopic)) {
renderings.push({ body: htmlTopic!, mimetype: "text/html" });
renderings.push({ body: htmlTopic, mimetype: "text/html" });
}
return { topic, [M_TOPIC.name]: renderings };
};
export type TopicState = {
text: string;
text?: string;
html?: string;
};
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
const mtopic = M_TOPIC.findIn<MTopicContent>(content);
if (!Array.isArray(mtopic)) {
return { text: content.topic };
return { text: content.topic ?? undefined };
}
const text = mtopic?.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic;
const text =
mtopic?.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic ?? undefined;
const html = mtopic?.find((r) => r.mimetype === "text/html")?.body;
return { text, html };
};
+59
View File
@@ -90,4 +90,63 @@ export enum CryptoEvent {
* `progress === total === -1`.
*/
LegacyCryptoStoreMigrationProgress = "crypto.legacyCryptoStoreMigrationProgress",
/**
* Fires when a new dehydrated device is created locally.
*
* After the client calls {@link CryptoApi.startDehydration}, this event
* will be fired every time a new dehydrated device is created. It may fire
* before `startDehydration` returns.
*/
DehydratedDeviceCreated = "dehydration.DehydratedDeviceCreated",
/**
* Fires when a new dehydrated device is successfully uploaded to the server.
*
* This should fire shortly after {@link DehydratedDeviceCreated} fires. If
* upload is unsuccessful, this will be reported either by an error thrown
* by {@link CryptoApi.startDehydration} (for errors that happen before
* `startDehydration` returns), or by firing {@link DehydratedDeviceRotationError}
* (for errors that happen during regular rotation of the dehydrated device)
*/
DehydratedDeviceUploaded = "dehydration.DehydratedDeviceUploaded",
/**
* Fires when rehydration has started.
*
* After the client calls {@link CryptoApi.startDehydration}, this event will
* fire if a dehydrated device is found and we attempt to rehydrate it.
*/
RehydrationStarted = "dehydration.RehydrationStarted",
/**
* Fires during rehydration, to inform the application of rehydration progress.
*
* The payload is a pair `[roomKeyCount: number, toDeviceCount: number]`,
* where `roomKeyCount` is the number of room keys that have been received
* so far, and `toDeviceCount` is the number of to-device messages received
* so far (including the messages containing room keys).
*/
RehydrationProgress = "dehydration.RehydrationProgress",
/** Fires when rehydration has completed successfully. */
RehydrationCompleted = "dehydration.RehydrationCompleted",
/** Fires when there was an error in rehydration.
*
* The payload is an error message as a string.
*/
RehydrationError = "dehydration.RehydrationError",
/**
* Fires when a dehydrated device key has been cached in the local database.
*/
DehydrationKeyCached = "dehydration.DehydrationKeyCached",
/**
* Fires when an error occurs during periodic rotation of the dehydrated device.
*
* The payload is an error message as a string.
*/
DehydratedDeviceRotationError = "dehydration.DehydratedDeviceRotationError",
}
+8
View File
@@ -30,4 +30,12 @@ export type CryptoEventHandlerMap = {
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
[CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void;
[CryptoEvent.DehydratedDeviceCreated]: () => void;
[CryptoEvent.DehydratedDeviceUploaded]: () => void;
[CryptoEvent.RehydrationStarted]: () => void;
[CryptoEvent.RehydrationProgress]: (roomKeyCount: number, toDeviceCount: number) => void;
[CryptoEvent.RehydrationCompleted]: () => void;
[CryptoEvent.RehydrationError]: (msg: string) => void;
[CryptoEvent.DehydrationKeyCached]: () => void;
[CryptoEvent.DehydratedDeviceRotationError]: (msg: string) => void;
} & RustBackupCryptoEventMap;
+60 -6
View File
@@ -41,6 +41,37 @@ import { MatrixEvent } from "../models/event.ts";
* @packageDocumentation
*/
/**
* The options to start device dehydration.
*/
export interface StartDehydrationOpts {
/**
* Force creation of a new dehydration key, even if there is already an
* existing dehydration key. If `false`, and `onlyIfKeyCached` is `false`, a
* new key will be created if there is no existing dehydration key, whether
* already cached in our local storage or stored in Secret Storage.
*
* Checking for the presence of the key in Secret Storage may result in the
* `getSecretStorageKey` callback being called.
*
* Defaults to `false`.
*/
createNewKey?: boolean;
/**
* Only start dehydration if we have a dehydration key cached in our local
* storage. If `true`, Secret Storage will not be checked. Defaults to
* `false`.
*/
onlyIfKeyCached?: boolean;
/**
* Try to rehydrate a device before creating a new dehydrated device.
* Setting this to `false` may be useful for situations where the client is
* known to pre-date the dehydrated device, and so rehydration is
* unnecessary. Defaults to `true`.
*/
rehydrate?: boolean;
}
/**
* Public interface to the cryptography parts of the js-sdk
*
@@ -221,6 +252,15 @@ export interface CryptoApi {
*/
pinCurrentUserIdentity(userId: string): Promise<void>;
/**
* Remove the requirement for this identity to be verified, and pin it.
*
* This is useful if the user was previously verified but is not anymore
* ({@link UserVerificationStatus.wasCrossSigningVerified}) and it is not possible to verify him again now.
*
*/
withdrawVerificationRequirement(userId: string): Promise<void>;
/**
* Get the verification status of a given device.
*
@@ -396,6 +436,19 @@ export interface CryptoApi {
payload: ToDevicePayload,
): Promise<ToDeviceBatch>;
/**
* Reset the encryption of the user by going through the following steps:
* - Disable backing up room keys and delete any existing backups.
* - Remove the default secret storage key from the account data (ie: the recovery key).
* - Reset the cross-signing keys.
* - Create a new key backup.
*
* @param authUploadDeviceSigningKeys - Callback to authenticate the upload of device signing keys.
* Used when resetting the cross signing keys.
* See {@link BootstrapCrossSigningOpts#authUploadDeviceSigningKeys}.
*/
resetEncryption(authUploadDeviceSigningKeys: UIAuthCallback<void>): Promise<void>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Device/User verification
@@ -627,10 +680,11 @@ export interface CryptoApi {
/**
* Start using device dehydration.
*
* - Rehydrates a dehydrated device, if one is available.
* - Rehydrates a dehydrated device, if one is available and `opts.rehydrate`
* is `true`.
* - Creates a new dehydration key, if necessary, and stores it in Secret
* Storage.
* - If `createNewKey` is set to true, always creates a new key.
* - If `opts.createNewKey` is set to true, always creates a new key.
* - If a dehydration key is not available, creates a new one.
* - Creates a new dehydrated device, and schedules periodically creating
* new dehydrated devices.
@@ -639,11 +693,11 @@ export interface CryptoApi {
* `true`, and must not be called until after cross-signing and secret
* storage have been set up.
*
* @param createNewKey - whether to force creation of a new dehydration key.
* This can be used, for example, if Secret Storage is being reset. Defaults
* to false.
* @param opts - options for device dehydration. For backwards compatibility
* with old code, a boolean can be given here, which will be treated as
* the `createNewKey` option. However, this is deprecated.
*/
startDehydration(createNewKey?: boolean): Promise<void>;
startDehydration(opts?: StartDehydrationOpts | boolean): Promise<void>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
+16 -1
View File
@@ -105,6 +105,7 @@ import {
CryptoEventHandlerMap as CryptoApiCryptoEventHandlerMap,
KeyBackupRestoreResult,
KeyBackupRestoreOpts,
StartDehydrationOpts,
} from "../crypto-api/index.ts";
import { Device, DeviceMap } from "../models/device.ts";
import { deviceInfoToDevice } from "./device-converter.ts";
@@ -1576,6 +1577,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
throw new Error("not implemented");
}
/**
* Implementation of {@link Crypto.CryptoApi.withdrawVerificationRequirement}.
*/
public async withdrawVerificationRequirement(userId: string): Promise<void> {
throw new Error("not implemented");
}
/**
* Check whether a given device is trusted.
*
@@ -4320,7 +4328,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* Stub function -- dehydration is not implemented here, so throw error
*/
public async startDehydration(createNewKey?: boolean): Promise<void> {
public async startDehydration(createNewKey?: StartDehydrationOpts | boolean): Promise<void> {
throw new Error("Not implemented");
}
@@ -4340,6 +4348,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
): Promise<KeyBackupRestoreResult> {
throw new Error("Not implemented");
}
/**
* Stub function -- resetEncryption is not implemented here, so throw error
*/
public resetEncryption(): Promise<void> {
throw new Error("Not implemented");
}
}
/**
+1 -1
View File
@@ -21,7 +21,7 @@ import { Optional } from "matrix-events-sdk";
* @param s - The optional to test.
* @returns True if the value is defined.
*/
export function isProvided<T>(s: Optional<T>): boolean {
export function isProvided<T>(s: Optional<T>): s is T {
return s !== null && s !== undefined;
}
+4 -1
View File
@@ -49,6 +49,7 @@ export * from "./scheduler.ts";
export * from "./filter.ts";
export * from "./timeline-window.ts";
export * from "./interactive-auth.ts";
export * from "./version-support.ts";
export * from "./service-types.ts";
export * from "./store/memory.ts";
export * from "./store/indexeddb.ts";
@@ -79,6 +80,8 @@ export type * from "./@types/IIdentityServerProvider.ts";
export * from "./@types/membership.ts";
export * from "./models/room-summary.ts";
export * from "./models/event-status.ts";
export * from "./models/profile-keys.ts";
export * from "./models/related-relations.ts";
export type { RoomSummary } from "./client.ts";
export * as ContentHelpers from "./content-helpers.ts";
export * as SecretStorage from "./secret-storage.ts";
@@ -106,7 +109,7 @@ export { CallFeedEvent } from "./webrtc/callFeed.ts";
export { StatsReport } from "./webrtc/stats/statsReport.ts";
export { Relations, RelationsEvent } from "./models/relations.ts";
export { TypedEventEmitter } from "./models/typed-event-emitter.ts";
export { LocalStorageErrors } from "./store/local-storage-events-emitter.ts";
export { LocalStorageErrors, localStorageErrorsEventsEmitter } from "./store/local-storage-events-emitter.ts";
export { IdentityProviderBrand, SSOAction } from "./@types/auth.ts";
export type { ISSOFlow as SSOFlow, LoginFlow } from "./@types/auth.ts";
export type { IHierarchyRelation as HierarchyRelation, IHierarchyRoom as HierarchyRoom } from "./@types/spaces.ts";
+1
View File
@@ -3,5 +3,6 @@
* match a timezone provided in https://www.iana.org/time-zones.
*
* @see https://github.com/matrix-org/matrix-spec-proposals/blob/clokep/profile-tz/proposals/4175-profile-field-time-zone.md
* @experimental
*/
export const ProfileKeyMSC4175Timezone = "us.cloke.msc4175.tz";
+2 -2
View File
@@ -23,7 +23,7 @@ import {
BearerTokenResponse,
UserState,
validateBearerTokenResponse,
ValidatedIssuerMetadata,
ValidatedAuthMetadata,
validateIdToken,
validateStoredUserState,
} from "./validate.ts";
@@ -138,7 +138,7 @@ export const generateOidcAuthorizationUrl = async ({
urlState,
}: {
clientId: string;
metadata: ValidatedIssuerMetadata;
metadata: ValidatedAuthMetadata;
homeserverUrl: string;
identityServerUrl?: string;
redirectUri: string;
+16 -9
View File
@@ -16,7 +16,7 @@ limitations under the License.
import { MetadataService, OidcClientSettingsStore } from "oidc-client-ts";
import { isValidatedIssuerMetadata, validateOIDCIssuerWellKnown } from "./validate.ts";
import { validateAuthMetadata } from "./validate.ts";
import { Method, timeoutSignal } from "../http-api/index.ts";
import { OidcClientConfig } from "./index.ts";
@@ -30,6 +30,7 @@ import { OidcClientConfig } from "./index.ts";
* @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
* @deprecated in favour of {@link MatrixClient#getAuthMetadata}
*/
export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Promise<OidcClientConfig> => {
const issuerOpenIdConfigUrl = new URL(".well-known/openid-configuration", issuer);
@@ -38,23 +39,29 @@ export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Pr
signal: timeoutSignal(5000),
});
const issuerWellKnown = await issuerWellKnownResponse.json();
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown);
return validateAuthMetadataAndKeys(issuerWellKnown);
};
/**
* @experimental
* Validate the authentication metadata and fetch the signing keys from the jwks_uri in the metadata
* @param authMetadata - the authentication metadata to validate
* @returns validated authentication metadata and signing keys
*/
export const validateAuthMetadataAndKeys = async (authMetadata: unknown): Promise<OidcClientConfig> => {
const validatedIssuerConfig = validateAuthMetadata(authMetadata);
// create a temporary settings store, so we can use metadata service for discovery
const settings = new OidcClientSettingsStore({
authority: issuer,
authority: validatedIssuerConfig.issuer,
metadata: validatedIssuerConfig,
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
});
const metadataService = new MetadataService(settings);
const metadata = await metadataService.getMetadata();
const signingKeys = (await metadataService.getSigningKeys()) ?? undefined;
isValidatedIssuerMetadata(metadata);
return {
...validatedIssuerConfig,
metadata,
signingKeys,
signingKeys: await metadataService.getSigningKeys(),
};
};
+3 -4
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import type { SigningKey } from "oidc-client-ts";
import { ValidatedIssuerConfig, ValidatedIssuerMetadata } from "./validate.ts";
import { ValidatedAuthMetadata } from "./validate.ts";
export * from "./authorize.ts";
export * from "./discovery.ts";
@@ -28,7 +28,6 @@ export * from "./validate.ts";
* 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[];
export interface OidcClientConfig extends ValidatedAuthMetadata {
signingKeys: SigningKey[] | null;
}
+3 -3
View File
@@ -65,12 +65,12 @@ export const registerOidcClient = async (
delegatedAuthConfig: OidcClientConfig,
clientMetadata: OidcRegistrationClientMetadata,
): Promise<string> => {
if (!delegatedAuthConfig.registrationEndpoint) {
if (!delegatedAuthConfig.registration_endpoint) {
throw new Error(OidcError.DynamicRegistrationNotSupported);
}
const grantTypes: NonEmptyArray<string> = ["authorization_code", "refresh_token"];
if (grantTypes.some((scope) => !delegatedAuthConfig.metadata.grant_types_supported.includes(scope))) {
if (grantTypes.some((scope) => !delegatedAuthConfig.grant_types_supported.includes(scope))) {
throw new Error(OidcError.DynamicRegistrationNotSupported);
}
@@ -95,7 +95,7 @@ export const registerOidcClient = async (
};
try {
const response = await fetch(delegatedAuthConfig.registrationEndpoint, {
const response = await fetch(delegatedAuthConfig.registration_endpoint, {
method: Method.Post,
headers,
body: JSON.stringify(metadata),
+3 -2
View File
@@ -77,11 +77,12 @@ export class OidcTokenRefresher {
const scope = generateScope(deviceId);
this.oidcClient = new OidcClient({
...config.metadata,
metadata: config,
signingKeys: config.signingKeys ?? undefined,
client_id: clientId,
scope,
redirect_uri: redirectUri,
authority: config.metadata.issuer,
authority: config.issuer,
stateStore: new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }),
});
} catch (error) {
+38 -62
View File
@@ -20,13 +20,28 @@ import { IdTokenClaims, OidcMetadata, SigninResponse } from "oidc-client-ts";
import { logger } from "../logger.ts";
import { OidcError } from "./error.ts";
export type ValidatedIssuerConfig = {
authorizationEndpoint: string;
tokenEndpoint: string;
registrationEndpoint?: string;
accountManagementEndpoint?: string;
accountManagementActionsSupported?: string[];
};
/**
* Metadata from OIDC authority discovery
* With validated properties required in type
*/
export type ValidatedAuthMetadata = Partial<OidcMetadata> &
Pick<
OidcMetadata,
| "issuer"
| "authorization_endpoint"
| "token_endpoint"
| "revocation_endpoint"
| "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[];
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported`
// even though it is part of the OIDC spec
prompt_values_supported?: string[];
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === "object" && !Array.isArray(value);
@@ -67,78 +82,39 @@ const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, val
* Validates issuer `.well-known/openid-configuration`
* As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html
* validates that OP is compatible with Element's OIDC flow
* @param wellKnown - json object
* @param authMetadata - json object
* @returns valid issuer config
* @throws Error - when issuer config is not found or is invalid
*/
export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuerConfig => {
if (!isRecord(wellKnown)) {
export const validateAuthMetadata = (authMetadata: unknown): ValidatedAuthMetadata => {
if (!isRecord(authMetadata)) {
logger.error("Issuer configuration not found or malformed");
throw new Error(OidcError.OpSupport);
}
const isInvalid = [
requiredStringProperty(wellKnown, "authorization_endpoint"),
requiredStringProperty(wellKnown, "token_endpoint"),
requiredStringProperty(wellKnown, "revocation_endpoint"),
optionalStringProperty(wellKnown, "registration_endpoint"),
optionalStringProperty(wellKnown, "account_management_uri"),
optionalStringProperty(wellKnown, "device_authorization_endpoint"),
optionalStringArrayProperty(wellKnown, "account_management_actions_supported"),
requiredArrayValue(wellKnown, "response_types_supported", "code"),
requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"),
requiredArrayValue(wellKnown, "code_challenge_methods_supported", "S256"),
requiredStringProperty(authMetadata, "issuer"),
requiredStringProperty(authMetadata, "authorization_endpoint"),
requiredStringProperty(authMetadata, "token_endpoint"),
requiredStringProperty(authMetadata, "revocation_endpoint"),
optionalStringProperty(authMetadata, "registration_endpoint"),
optionalStringProperty(authMetadata, "account_management_uri"),
optionalStringProperty(authMetadata, "device_authorization_endpoint"),
optionalStringArrayProperty(authMetadata, "account_management_actions_supported"),
requiredArrayValue(authMetadata, "response_types_supported", "code"),
requiredArrayValue(authMetadata, "grant_types_supported", "authorization_code"),
requiredArrayValue(authMetadata, "code_challenge_methods_supported", "S256"),
optionalStringArrayProperty(authMetadata, "prompt_values_supported"),
].some((isValid) => !isValid);
if (!isInvalid) {
return {
authorizationEndpoint: <string>wellKnown["authorization_endpoint"],
tokenEndpoint: <string>wellKnown["token_endpoint"],
registrationEndpoint: <string>wellKnown["registration_endpoint"],
accountManagementEndpoint: <string>wellKnown["account_management_uri"],
accountManagementActionsSupported: <string[]>wellKnown["account_management_actions_supported"],
};
return authMetadata as ValidatedAuthMetadata;
}
logger.error("Issuer configuration not valid");
throw new Error(OidcError.OpSupport);
};
/**
* Metadata from OIDC authority discovery
* With validated properties required in type
*/
export type ValidatedIssuerMetadata = Partial<OidcMetadata> &
Pick<
OidcMetadata,
| "issuer"
| "authorization_endpoint"
| "token_endpoint"
| "registration_endpoint"
| "revocation_endpoint"
| "response_types_supported"
| "grant_types_supported"
| "code_challenge_methods_supported"
| "device_authorization_endpoint"
> & {
// MSC2965 extensions to the OIDC spec
account_management_uri?: string;
account_management_actions_supported?: string[];
};
/**
* Wraps validateOIDCIssuerWellKnown in a type assertion
* that asserts expected properties are present
* (Typescript assertions cannot be arrow functions)
* @param metadata - issuer openid-configuration response
* @throws when metadata validation fails
*/
export function isValidatedIssuerMetadata(
metadata: Partial<OidcMetadata>,
): asserts metadata is ValidatedIssuerMetadata {
validateOIDCIssuerWellKnown(metadata);
}
export const decodeIdToken = (token: string): IdTokenClaims => {
try {
return jwtDecode<IdTokenClaims>(token);
+3 -4
View File
@@ -27,7 +27,7 @@ import { logger } from "../logger.ts";
import { MSC4108SecureChannel } from "./channels/MSC4108SecureChannel.ts";
import { MatrixError } from "../http-api/index.ts";
import { sleep } from "../utils.ts";
import { DEVICE_CODE_SCOPE, discoverAndValidateOIDCIssuerWellKnown, OidcClientConfig } from "../oidc/index.ts";
import { DEVICE_CODE_SCOPE, OidcClientConfig } from "../oidc/index.ts";
import { CryptoApi } from "../crypto-api/index.ts";
/**
@@ -189,13 +189,12 @@ export class MSC4108SignInWithQR {
// MSC4108-Flow: NewScanned -send protocols message
let oidcClientConfig: OidcClientConfig | undefined;
try {
const { issuer } = await this.client!.getAuthIssuer();
oidcClientConfig = await discoverAndValidateOIDCIssuerWellKnown(issuer);
oidcClientConfig = await this.client!.getAuthMetadata();
} catch (e) {
logger.error("Failed to discover OIDC metadata", e);
}
if (oidcClientConfig?.metadata.grant_types_supported.includes(DEVICE_CODE_SCOPE)) {
if (oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE)) {
await this.send<ProtocolsPayload>({
type: PayloadType.Protocols,
protocols: ["device_authorization_grant"],
+91 -34
View File
@@ -21,8 +21,10 @@ import { encodeUri } from "../utils.ts";
import { IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts";
import { IToDeviceEvent } from "../sync-accumulator.ts";
import { ServerSideSecretStorage } from "../secret-storage.ts";
import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
import { decodeBase64 } from "../base64.ts";
import { Logger } from "../logger.ts";
import { CryptoEvent, CryptoEventHandlerMap, StartDehydrationOpts } from "../crypto-api/index.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
/**
* The response body of `GET /_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device`.
@@ -67,9 +69,7 @@ const DEHYDRATION_INTERVAL = 7 * 24 * 60 * 60 * 1000;
*
* @internal
*/
export class DehydratedDeviceManager {
/** the secret key used for dehydrating and rehydrating */
private key?: Uint8Array;
export class DehydratedDeviceManager extends TypedEventEmitter<DehydratedDevicesEvents, DehydratedDevicesEventMap> {
/** the ID of the interval for periodically replacing the dehydrated device */
private intervalId?: ReturnType<typeof setInterval>;
@@ -79,7 +79,14 @@ export class DehydratedDeviceManager {
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
private readonly secretStorage: ServerSideSecretStorage,
) {}
) {
super();
}
private async cacheKey(key: RustSdkCryptoJs.DehydratedDeviceKey): Promise<void> {
await this.olmMachine.dehydratedDevices().saveDehydratedDeviceKey(key);
this.emit(CryptoEvent.DehydrationKeyCached);
}
/**
* Return whether the server supports dehydrated devices.
@@ -114,27 +121,39 @@ export class DehydratedDeviceManager {
/**
* Start using device dehydration.
*
* - Rehydrates a dehydrated device, if one is available.
* - Rehydrates a dehydrated device, if one is available and `opts.rehydrate`
* is `true`.
* - Creates a new dehydration key, if necessary, and stores it in Secret
* Storage.
* - If `createNewKey` is set to true, always creates a new key.
* - If `opts.createNewKey` is set to true, always creates a new key.
* - If a dehydration key is not available, creates a new one.
* - Creates a new dehydrated device, and schedules periodically creating
* new dehydrated devices.
*
* @param createNewKey - whether to force creation of a new dehydration key.
* This can be used, for example, if Secret Storage is being reset.
* @param opts - options for device dehydration. For backwards compatibility
* with old code, a boolean can be given here, which will be treated as
* the `createNewKey` option. However, this is deprecated.
*/
public async start(createNewKey?: boolean): Promise<void> {
this.stop();
try {
await this.rehydrateDeviceIfAvailable();
} catch (e) {
// If rehydration fails, there isn't much we can do about it. Log
// the error, and create a new device.
this.logger.info("dehydration: Error rehydrating device:", e);
public async start(opts: StartDehydrationOpts | boolean = {}): Promise<void> {
if (typeof opts === "boolean") {
opts = { createNewKey: opts };
}
if (createNewKey) {
if (opts.onlyIfKeyCached && !(await this.olmMachine.dehydratedDevices().getDehydratedDeviceKey())) {
return;
}
this.stop();
if (opts.rehydrate !== false) {
try {
await this.rehydrateDeviceIfAvailable();
} catch (e) {
// If rehydration fails, there isn't much we can do about it. Log
// the error, and create a new device.
this.logger.info("dehydration: Error rehydrating device:", e);
this.emit(CryptoEvent.RehydrationError, (e as Error).message);
}
}
if (opts.createNewKey) {
await this.resetKey();
}
await this.scheduleDeviceDehydration();
@@ -151,12 +170,15 @@ export class DehydratedDeviceManager {
* Reset the dehydration key.
*
* Creates a new key and stores it in secret storage.
*
* @returns The newly-generated key.
*/
public async resetKey(): Promise<void> {
const key = new Uint8Array(32);
globalThis.crypto.getRandomValues(key);
await this.secretStorage.store(SECRET_STORAGE_NAME, encodeUnpaddedBase64(key));
this.key = key;
public async resetKey(): Promise<RustSdkCryptoJs.DehydratedDeviceKey> {
const key = RustSdkCryptoJs.DehydratedDeviceKey.createRandomKey();
await this.secretStorage.store(SECRET_STORAGE_NAME, key.toBase64());
// Also cache it in the rust SDK's crypto store.
await this.cacheKey(key);
return key;
}
/**
@@ -166,19 +188,27 @@ export class DehydratedDeviceManager {
*
* @returns the key, if available, or `null` if no key is available
*/
private async getKey(create: boolean): Promise<Uint8Array | null> {
if (this.key === undefined) {
const keyB64 = await this.secretStorage.get(SECRET_STORAGE_NAME);
if (keyB64 === undefined) {
if (!create) {
return null;
}
await this.resetKey();
} else {
this.key = decodeBase64(keyB64);
private async getKey(create: boolean): Promise<RustSdkCryptoJs.DehydratedDeviceKey | null> {
const cachedKey = await this.olmMachine.dehydratedDevices().getDehydratedDeviceKey();
if (cachedKey) return cachedKey;
const keyB64 = await this.secretStorage.get(SECRET_STORAGE_NAME);
if (keyB64 === undefined) {
if (!create) {
return null;
}
return await this.resetKey();
}
// We successfully found the key in secret storage: decode it, and cache it in
// the rust SDK's crypto store.
const bytes = decodeBase64(keyB64);
try {
const key = RustSdkCryptoJs.DehydratedDeviceKey.createKeyFromArray(bytes);
await this.cacheKey(key);
return key;
} finally {
bytes.fill(0);
}
return this.key!;
}
/**
@@ -219,6 +249,7 @@ export class DehydratedDeviceManager {
}
this.logger.info("dehydration: dehydrated device found");
this.emit(CryptoEvent.RehydrationStarted);
const rehydratedDevice = await this.olmMachine
.dehydratedDevices()
@@ -255,8 +286,11 @@ export class DehydratedDeviceManager {
nextBatch = eventResp.next_batch;
const roomKeyInfos = await rehydratedDevice.receiveEvents(JSON.stringify(eventResp.events));
roomKeyCount += roomKeyInfos.length;
this.emit(CryptoEvent.RehydrationProgress, roomKeyCount, toDeviceCount);
}
this.logger.info(`dehydration: received ${roomKeyCount} room keys from ${toDeviceCount} to-device events`);
this.emit(CryptoEvent.RehydrationCompleted);
return true;
}
@@ -270,9 +304,11 @@ export class DehydratedDeviceManager {
const key = (await this.getKey(true))!;
const dehydratedDevice = await this.olmMachine.dehydratedDevices().create();
this.emit(CryptoEvent.DehydratedDeviceCreated);
const request = await dehydratedDevice.keysForUpload("Dehydrated device", key);
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
this.emit(CryptoEvent.DehydratedDeviceUploaded);
this.logger.info("dehydration: uploaded device");
}
@@ -287,6 +323,7 @@ export class DehydratedDeviceManager {
await this.createAndUploadDehydratedDevice();
this.intervalId = setInterval(() => {
this.createAndUploadDehydratedDevice().catch((error) => {
this.emit(CryptoEvent.DehydratedDeviceRotationError, error.message);
this.logger.error("Error creating dehydrated device:", error);
});
}, DEHYDRATION_INTERVAL);
@@ -304,3 +341,23 @@ export class DehydratedDeviceManager {
}
}
}
/**
* The events fired by the DehydratedDeviceManager
* @internal
*/
type DehydratedDevicesEvents =
| CryptoEvent.DehydratedDeviceCreated
| CryptoEvent.DehydratedDeviceUploaded
| CryptoEvent.RehydrationStarted
| CryptoEvent.RehydrationProgress
| CryptoEvent.RehydrationCompleted
| CryptoEvent.RehydrationError
| CryptoEvent.DehydrationKeyCached
| CryptoEvent.DehydratedDeviceRotationError;
/**
* A map of the {@link DehydratedDeviceEvents} fired by the {@link DehydratedDeviceManager} and their payloads.
* @internal
*/
type DehydratedDevicesEventMap = Pick<CryptoEventHandlerMap, DehydratedDevicesEvents>;
+53 -2
View File
@@ -67,6 +67,7 @@ import {
CryptoEventHandlerMap,
KeyBackupRestoreOpts,
KeyBackupRestoreResult,
StartDehydrationOpts,
} from "../crypto-api/index.ts";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
@@ -88,6 +89,7 @@ import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader.t
import { DehydratedDeviceManager } from "./DehydratedDeviceManager.ts";
import { VerificationMethod } from "../types.ts";
import { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
import { UIAuthCallback } from "../interactive-auth.ts";
const ALL_VERIFICATION_METHODS = [
VerificationMethod.Sas,
@@ -182,12 +184,23 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
);
this.eventDecryptor = new EventDecryptor(this.logger, olmMachine, this.perSessionBackupDownloader);
// re-emit the events emitted by managers
this.reemitter.reEmit(this.backupManager, [
CryptoEvent.KeyBackupStatus,
CryptoEvent.KeyBackupSessionsRemaining,
CryptoEvent.KeyBackupFailed,
CryptoEvent.KeyBackupDecryptionKeyCached,
]);
this.reemitter.reEmit(this.dehydratedDeviceManager, [
CryptoEvent.DehydratedDeviceCreated,
CryptoEvent.DehydratedDeviceUploaded,
CryptoEvent.RehydrationStarted,
CryptoEvent.RehydrationProgress,
CryptoEvent.RehydrationCompleted,
CryptoEvent.RehydrationError,
CryptoEvent.DehydrationKeyCached,
CryptoEvent.DehydratedDeviceRotationError,
]);
this.crossSigningIdentity = new CrossSigningIdentity(olmMachine, this.outgoingRequestProcessor, secretStorage);
@@ -687,6 +700,20 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
await userIdentity.pinCurrentMasterKey();
}
/**
* Implementation of {@link CryptoApi#withdrawVerificationRequirement}.
*/
public async withdrawVerificationRequirement(userId: string): Promise<void> {
const userIdentity: RustSdkCryptoJs.OtherUserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(userId));
if (userIdentity === undefined) {
throw new Error("Cannot withdraw verification of unknown user");
}
await userIdentity.withdrawVerification();
}
/**
* Implementation of {@link CryptoApi#isCrossSigningReady}
*/
@@ -1399,11 +1426,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/**
* Implementation of {@link CryptoApi#startDehydration}.
*/
public async startDehydration(createNewKey?: boolean): Promise<void> {
public async startDehydration(opts: StartDehydrationOpts | boolean = {}): Promise<void> {
if (!(await this.isCrossSigningReady()) || !(await this.isSecretStorageReady())) {
throw new Error("Device dehydration requires cross-signing and secret storage to be set up");
}
return await this.dehydratedDeviceManager.start(createNewKey);
return await this.dehydratedDeviceManager.start(opts || {});
}
/**
@@ -1472,6 +1499,30 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
return batch;
}
/**
* Implementation of {@link CryptoApi#resetEncryption}.
*/
public async resetEncryption(authUploadDeviceSigningKeys: UIAuthCallback<void>): Promise<void> {
this.logger.debug("resetEncryption: resetting encryption");
// Disable backup, and delete all the backups from the server
await this.backupManager.deleteAllKeyBackupVersions();
// Disable the recovery key and the secret storage
await this.secretStorage.setDefaultKeyId(null);
// Reset the cross-signing keys
await this.crossSigningIdentity.bootstrapCrossSigning({
setupNewCrossSigning: true,
authUploadDeviceSigningKeys,
});
// Create a new key backup
await this.resetKeyBackup();
this.logger.debug("resetEncryption: ended");
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// SyncCryptoCallbacks implementation
+43
View File
@@ -27,6 +27,7 @@ import { RoomMember } from "./models/room-member.ts";
import { EventType } from "./@types/event.ts";
import { DecryptionFailureCode } from "./crypto-api/index.ts";
import { DecryptionError, EventDecryptionResult } from "./common-crypto/CryptoBackend.ts";
import { OidcClientConfig, ValidatedAuthMetadata } from "./oidc/index.ts";
/**
* Create a {@link MatrixEvent}.
@@ -188,3 +189,45 @@ export async function decryptExistingEvent(
} as Parameters<MatrixEvent["attemptDecryption"]>[0];
await mxEvent.attemptDecryption(mockCrypto);
}
/**
* Makes a valid OidcClientConfig with minimum valid values
* @param issuer used as the base for all other urls
* @param additionalGrantTypes to add to the default grant types
* @returns OidcClientConfig
* @experimental
*/
export const makeDelegatedAuthConfig = (
issuer = "https://auth.org/",
additionalGrantTypes: string[] = [],
): OidcClientConfig => {
const metadata = mockOpenIdConfiguration(issuer, additionalGrantTypes);
return {
...metadata,
signingKeys: null,
};
};
/**
* Useful for mocking <issuer>/.well-known/openid-configuration
* @param issuer used as the base for all other urls
* @param additionalGrantTypes to add to the default grant types
* @returns ValidatedAuthMetadata
* @experimental
*/
export const mockOpenIdConfiguration = (
issuer = "https://auth.org/",
additionalGrantTypes: string[] = [],
): ValidatedAuthMetadata => ({
issuer,
revocation_endpoint: issuer + "revoke",
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
code_challenge_methods_supported: ["S256"],
});
+116 -95
View File
@@ -82,9 +82,9 @@
semver "^6.3.1"
"@babel/eslint-parser@^7.12.10":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.25.9.tgz#603c68a63078796527bc9d0833f5e52dd5f9224c"
integrity sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ==
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.26.5.tgz#aa669f4d873f9cd617050cf3c40c19cd96307efb"
integrity sha512-Kkm8C8uxI842AwQADxl0GbcG1rupELYLShazYEZO/2DYjhyWXJIOUVOE3tBYm6JXzUCNJOZEzqc4rCW/jsEQYQ==
dependencies:
"@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1"
eslint-visitor-keys "^2.1.0"
@@ -1477,10 +1477,10 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@matrix-org/matrix-sdk-crypto-wasm@^12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.1.0.tgz#2aef64eab2d30c0a1ace9c0fe876f53aa2949f14"
integrity sha512-NhJFu/8FOGjnW7mDssRUzaMSwXrYOcCqgAjZyAw9KQ9unNADKEi7KoIKe7GtrG2PWtm36y2bUf+hB8vhSY6Wdw==
"@matrix-org/matrix-sdk-crypto-wasm@^13.0.0":
version "13.0.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-13.0.0.tgz#658bed951e4c8a06a6dd545575a79cf32022d4ba"
integrity sha512-2gtpjnxL42sdJAgkwitpMMI4cw7Gcjf5sW0MXoe+OAlXPlxIzyM+06F5JJ8ENvBeHkuV2RqtFIRrh8i90HLsMw==
"@matrix-org/olm@3.2.15":
version "3.2.15"
@@ -1522,12 +1522,33 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.scandir@4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-4.0.1.tgz#4b981a1d6e34e2baff9a33c84b57709e30e8ce30"
integrity sha512-vAkI715yhnmiPupY+dq+xenu5Tdf2TBQ66jLvBIcCddtz+5Q8LbMKaf9CIJJreez8fQ8fgaY+RaywQx8RJIWpw==
dependencies:
"@nodelib/fs.stat" "4.0.0"
run-parallel "^1.2.0"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@1.2.8", "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
"@nodelib/fs.stat@4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-4.0.0.tgz#d609b10d9c17dac012c4d7be93602ed939861e58"
integrity sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg==
"@nodelib/fs.walk@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-3.0.1.tgz#717a03eb1d07c09db32f884a5d0c06ccd9b657f4"
integrity sha512-nIh/M6Kh3ZtOmlY00DaUYB4xeeV6F3/ts1l29iwl3/cfyY/OuCfUx+v08zgx8TKPTifXRcjjqVQ4KB2zOYSbyw==
dependencies:
"@nodelib/fs.scandir" "4.0.1"
fastq "^1.15.0"
"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
@@ -1637,9 +1658,9 @@
p-map "^4.0.0"
"@stylistic/eslint-plugin@^2.9.0":
version "2.12.1"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.12.1.tgz#e341beb4e4315084d8be20bceeeda7d8a46f079f"
integrity sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ==
version "2.13.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.13.0.tgz#53bf175dac8c1ec055b370a6ff77d491cae9a70d"
integrity sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==
dependencies:
"@typescript-eslint/utils" "^8.13.0"
eslint-visitor-keys "^4.2.0"
@@ -1803,9 +1824,9 @@
undici-types "~5.26.4"
"@types/node@18":
version "18.19.70"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.70.tgz#5a77508f5568d16fcd3b711c8102d7a430a04df7"
integrity sha512-RE+K0+KZoEpDUbGGctnGdkrLFwi1eYKTlIHNl2Um98mUkGsm1u2Ff6Ltd0e8DktTtC98uy7rSj+hO8t/QuLoVQ==
version "18.19.71"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.71.tgz#96d4f0a0be735ead6c8998c62a4b2c0012a5d09a"
integrity sha512-evXpcgtZm8FY4jqBSN8+DmOTcVkkvTmAayeo4Wf3m1xAruyVGzGuDh/Fb/WWX2yLItUiho42ozyJjB0dw//Tkw==
dependencies:
undici-types "~5.26.4"
@@ -1857,61 +1878,61 @@
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^8.0.0":
version "8.19.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz#5f26c0a833b27bcb1aa402b82e76d3b8dda0b247"
integrity sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==
version "8.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz#395014a75112ecdb81142b866ab6bb62e3be0f2a"
integrity sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.19.1"
"@typescript-eslint/type-utils" "8.19.1"
"@typescript-eslint/utils" "8.19.1"
"@typescript-eslint/visitor-keys" "8.19.1"
"@typescript-eslint/scope-manager" "8.21.0"
"@typescript-eslint/type-utils" "8.21.0"
"@typescript-eslint/utils" "8.21.0"
"@typescript-eslint/visitor-keys" "8.21.0"
graphemer "^1.4.0"
ignore "^5.3.1"
natural-compare "^1.4.0"
ts-api-utils "^2.0.0"
"@typescript-eslint/parser@^8.0.0":
version "8.19.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.19.1.tgz#b836fcfe7a704c8c65f5a50e5b0ff8acfca5c21b"
integrity sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==
version "8.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.21.0.tgz#312c638aaba4f640d45bfde7c6795a9d75deb088"
integrity sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==
dependencies:
"@typescript-eslint/scope-manager" "8.19.1"
"@typescript-eslint/types" "8.19.1"
"@typescript-eslint/typescript-estree" "8.19.1"
"@typescript-eslint/visitor-keys" "8.19.1"
"@typescript-eslint/scope-manager" "8.21.0"
"@typescript-eslint/types" "8.21.0"
"@typescript-eslint/typescript-estree" "8.21.0"
"@typescript-eslint/visitor-keys" "8.21.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.19.1":
version "8.19.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz#794cfc8add4f373b9cd6fa32e367e7565a0e231b"
integrity sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==
"@typescript-eslint/scope-manager@8.21.0":
version "8.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz#d08d94e2a34b4ccdcc975543c25bb62917437500"
integrity sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==
dependencies:
"@typescript-eslint/types" "8.19.1"
"@typescript-eslint/visitor-keys" "8.19.1"
"@typescript-eslint/types" "8.21.0"
"@typescript-eslint/visitor-keys" "8.21.0"
"@typescript-eslint/type-utils@8.19.1":
version "8.19.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz#23710ab52643c19f74601b3f4a076c98f4e159aa"
integrity sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==
"@typescript-eslint/type-utils@8.21.0":
version "8.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz#2e69d1a93cdbedc73fe694cd6ae4dfedd00430a0"
integrity sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==
dependencies:
"@typescript-eslint/typescript-estree" "8.19.1"
"@typescript-eslint/utils" "8.19.1"
"@typescript-eslint/typescript-estree" "8.21.0"
"@typescript-eslint/utils" "8.21.0"
debug "^4.3.4"
ts-api-utils "^2.0.0"
"@typescript-eslint/types@8.19.1":
version "8.19.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.19.1.tgz#015a991281754ed986f2e549263a1188d6ed0a8c"
integrity sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==
"@typescript-eslint/types@8.21.0":
version "8.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.21.0.tgz#58f30aec8db8212fd886835dc5969cdf47cb29f5"
integrity sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==
"@typescript-eslint/typescript-estree@8.19.1":
version "8.19.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz#c1094bb00bc251ac76cf215569ca27236435036b"
integrity sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==
"@typescript-eslint/typescript-estree@8.21.0":
version "8.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz#5ce71acdbed3b97b959f6168afba5a03c88f69a9"
integrity sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==
dependencies:
"@typescript-eslint/types" "8.19.1"
"@typescript-eslint/visitor-keys" "8.19.1"
"@typescript-eslint/types" "8.21.0"
"@typescript-eslint/visitor-keys" "8.21.0"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
@@ -1919,22 +1940,22 @@
semver "^7.6.0"
ts-api-utils "^2.0.0"
"@typescript-eslint/utils@8.19.1", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.13.0":
version "8.19.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.19.1.tgz#dd8eabd46b92bf61e573286e1c0ba6bd243a185b"
integrity sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==
"@typescript-eslint/utils@8.21.0", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.13.0":
version "8.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.21.0.tgz#bc4874fbc30feb3298b926e3b03d94570b3999c5"
integrity sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@typescript-eslint/scope-manager" "8.19.1"
"@typescript-eslint/types" "8.19.1"
"@typescript-eslint/typescript-estree" "8.19.1"
"@typescript-eslint/scope-manager" "8.21.0"
"@typescript-eslint/types" "8.21.0"
"@typescript-eslint/typescript-estree" "8.21.0"
"@typescript-eslint/visitor-keys@8.19.1":
version "8.19.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz#fce54d7cfa5351a92387d6c0c5be598caee072e0"
integrity sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==
"@typescript-eslint/visitor-keys@8.21.0":
version "8.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz#a89744c4cdc83b5c761eb5878befe6c33d1481b2"
integrity sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==
dependencies:
"@typescript-eslint/types" "8.19.1"
"@typescript-eslint/types" "8.21.0"
eslint-visitor-keys "^4.2.0"
"@ungap/structured-clone@^1.2.0":
@@ -2820,7 +2841,7 @@ enhanced-resolve@^5.15.0:
graceful-fs "^4.2.4"
tapable "^2.2.0"
enhanced-resolve@^5.17.1:
enhanced-resolve@^5.18.0:
version "5.18.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz#91eb1db193896b9801251eeff1c6980278b1e404"
integrity sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==
@@ -3056,16 +3077,16 @@ eslint-plugin-import@^2.26.0:
tsconfig-paths "^3.15.0"
eslint-plugin-jest@^28.0.0:
version "28.10.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.10.0.tgz#4b35b8abb0f7cfe699bff8d9060270a2ddd770ea"
integrity sha512-hyMWUxkBH99HpXT3p8hc7REbEZK3D+nk8vHXGgpB+XXsi0gO4PxMSP+pjfUzb67GnV9yawV9a53eUmcde1CCZA==
version "28.11.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz#2641ecb4411941bbddb3d7cf8a8ff1163fbb510e"
integrity sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==
dependencies:
"@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0"
eslint-plugin-jsdoc@^50.0.0:
version "50.6.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.1.tgz#791a668fd4b0700a759e9a16a741a6a805f5b95c"
integrity sha512-UWyaYi6iURdSfdVVqvfOs2vdCVz0J40O/z/HTsv2sFjdjmdlUI/qlKLOTmwbPQ2tAfQnE5F9vqx+B+poF71DBQ==
version "50.6.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.2.tgz#9c95646d8d85a1b5ed87206695c8f10f2ee35537"
integrity sha512-n7GNZ4czMAAbDg7DsDA7PvHo1IPIUwAXYmxTx6j/hTlXbt5V0x5q/kGkiJ7s4wA9SpB/yaiK8jF7CO237lOLew==
dependencies:
"@es-joy/jsdoccomment" "~0.49.0"
are-docs-informative "^0.0.2"
@@ -3355,7 +3376,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.3.2:
fast-glob@^3.3.2, fast-glob@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
@@ -3376,7 +3397,7 @@ fast-levenshtein@^2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fastq@^1.6.0:
fastq@^1.15.0, fastq@^1.6.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.18.0.tgz#d631d7e25faffea81887fe5ea8c9010e1b36fee0"
integrity sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==
@@ -4519,7 +4540,7 @@ jest@^29.0.0:
import-local "^3.0.2"
jest-cli "^29.7.0"
jiti@^2.4.0:
jiti@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.2.tgz#d19b7732ebb6116b06e2038da74a55366faef560"
integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==
@@ -4656,16 +4677,16 @@ kleur@^3.0.3:
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
knip@^5.0.0:
version "5.41.1"
resolved "https://registry.yarnpkg.com/knip/-/knip-5.41.1.tgz#b6e27186d38e6bccd2ef8346294e78d13322f1cd"
integrity sha512-yNpCCe2REU7U3VRvMASnXSEtfEC2HmOoDW9Vp9teQ9FktJYnuagvSZD3xWq8Ru7sPABkmvbC5TVWuMzIaeADNA==
version "5.42.2"
resolved "https://registry.yarnpkg.com/knip/-/knip-5.42.2.tgz#7f2c2078440abe7612c166b6aecedb9b690bf34f"
integrity sha512-hVtZ6V59COFz3Y0/BHrWMlPAx82EdX/xFHXbutIRSNfJFPMGmIpxLBWTg35F4XQJGwlu5uWiJf8rBYYkmlYWWQ==
dependencies:
"@nodelib/fs.walk" "1.2.8"
"@nodelib/fs.walk" "3.0.1"
"@snyk/github-codeowners" "1.1.0"
easy-table "1.2.0"
enhanced-resolve "^5.17.1"
fast-glob "^3.3.2"
jiti "^2.4.0"
enhanced-resolve "^5.18.0"
fast-glob "^3.3.3"
jiti "^2.4.2"
js-yaml "^4.1.0"
minimist "^1.2.8"
picocolors "^1.1.0"
@@ -4708,9 +4729,9 @@ linkify-it@^5.0.0:
uc.micro "^2.0.0"
lint-staged@^15.0.2:
version "15.3.0"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.3.0.tgz#32a0b3f2f2b8825950bd3b9fb093e045353bdfa3"
integrity sha512-vHFahytLoF2enJklgtOtCtIjZrKD/LoxlaUusd5nh7dWv/dkKQJY74ndFSzxCdv7g0ueGg1ORgTSt4Y9LPZn9A==
version "15.4.1"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.4.1.tgz#b34e3297ae13fdb2d99b3456e2dbd8e20798bced"
integrity sha512-P8yJuVRyLrm5KxCtFx+gjI5Bil+wO7wnTl7C3bXhvtTaAFGirzeB24++D0wGoUwxrUKecNiehemgCob9YL39NA==
dependencies:
chalk "~5.4.1"
commander "~12.1.0"
@@ -5574,7 +5595,7 @@ rimraf@^6.0.0:
glob "^11.0.0"
package-json-from-dist "^1.0.0"
run-parallel@^1.1.9:
run-parallel@^1.1.9, run-parallel@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
@@ -5780,9 +5801,9 @@ spdx-expression-parse@^4.0.0:
spdx-license-ids "^3.0.0"
spdx-license-ids@^3.0.0:
version "3.0.20"
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz#e44ed19ed318dd1e5888f93325cee800f0f51b89"
integrity sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==
version "3.0.21"
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz#6d6e980c9df2b6fc905343a3b2d702a6239536c3"
integrity sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==
sprintf-js@~1.0.2:
version "1.0.3"
@@ -6171,9 +6192,9 @@ typedoc-plugin-coverage@^3.0.0:
integrity sha512-V23DAwinAMpGMGcL1R1s8Snr26hrjfIdwGf+4jR/65ZdmeAN+yRX0onfx5JlembTQhR6AePQ/parfQNS0AZ64A==
typedoc-plugin-mdn-links@^4.0.0:
version "4.0.7"
resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-4.0.7.tgz#0949bcca5bf615c4dc35abe653ebc30eade61980"
integrity sha512-S9nUdZShdu+8HyWygmxjqyYIviZVdL36OjPWvxuLVMrS21lqxnVYLqInPYHXalmvitVEqWmrJJk8Al0x6d8wEA==
version "4.0.9"
resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-4.0.9.tgz#5f91000d11e2fd0c6271fc05e76e08813184812a"
integrity sha512-udtolnpREnT+SB3xZnWHIyN2KtzjlohBOtOOYw+ofKMZKTCaYzHElSyaP2oS/7w/cZtFvrXr/m+35458XUcSLQ==
typedoc-plugin-missing-exports@^3.0.0:
version "3.1.0"
@@ -6192,9 +6213,9 @@ typedoc@^0.27.0:
yaml "^2.6.1"
typescript@^5.4.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
version "5.7.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e"
integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==
uc.micro@^2.0.0, uc.micro@^2.1.0:
version "2.1.0"
@@ -6273,9 +6294,9 @@ url-parse@^1.5.3:
requires-port "^1.0.0"
uuid@11:
version "11.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d"
integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==
version "11.0.5"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.5.tgz#07b46bdfa6310c92c3fb3953a8720f170427fc62"
integrity sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==
uuid@8.3.2:
version "8.3.2"