1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-30 04:23:07 +03:00

Modernize http-api - move from browser-request to fetch (#2719)

This commit is contained in:
Michael Telatynski
2022-10-12 18:59:04 +01:00
committed by GitHub
parent 913660c818
commit 34c5598a3f
56 changed files with 2528 additions and 2543 deletions

View File

@ -33,10 +33,8 @@ In Node.js
----------
Ensure you have the latest LTS version of Node.js installed.
This SDK targets Node 12 for compatibility, which translates to ES6. If you're using
a bundler like webpack you'll likely have to transpile dependencies, including this
SDK, to match your target browsers.
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.

View File

@ -3,7 +3,7 @@
"version": "20.1.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=12.9.0"
"node": ">=16.0.0"
},
"scripts": {
"prepublishOnly": "yarn build",
@ -55,14 +55,12 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"another-json": "^0.2.0",
"browser-request": "^0.3.3",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "^0.0.1-beta.7",
"p-retry": "4",
"qs": "^6.9.6",
"request": "^2.88.2",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
@ -81,9 +79,9 @@
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/domexception": "^4.0.0",
"@types/jest": "^29.0.0",
"@types/node": "16",
"@types/request": "^2.48.5",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
@ -92,6 +90,7 @@
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"domexception": "^4.0.0",
"eslint": "8.24.0",
"eslint-config-google": "^0.14.0",
"eslint-import-resolver-typescript": "^3.5.1",
@ -104,7 +103,7 @@
"jest-mock": "^27.5.1",
"jest-sonar-reporter": "^2.0.0",
"jsdoc": "^3.6.6",
"matrix-mock-request": "^2.1.2",
"matrix-mock-request": "^2.5.0",
"rimraf": "^3.0.2",
"terser": "^5.5.1",
"tsify": "^5.0.2",
@ -115,6 +114,9 @@
"testMatch": [
"<rootDir>/spec/**/*.spec.{js,ts}"
],
"setupFilesAfterEnv": [
"<rootDir>/spec/setupTests.ts"
],
"collectCoverageFrom": [
"<rootDir>/src/**/*.{js,ts}"
],

View File

@ -30,7 +30,6 @@ import { MockStorageApi } from "./MockStorageApi";
import { encodeUri } from "../src/utils";
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
import { IKeyBackupSession } from "../src/crypto/keybackup";
import { IHttpOpts } from "../src/http-api";
import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
/**
@ -56,11 +55,11 @@ export class TestClient {
this.httpBackend = new MockHttpBackend();
const fullOptions: ICreateClientOpts = {
baseUrl: "http://" + userId + ".test.server",
baseUrl: "http://" + userId?.slice(1).replace(":", ".") + ".test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
request: this.httpBackend.requestFn as IHttpOpts["request"],
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
...options,
};
if (!fullOptions.cryptoStore) {

View File

@ -14,46 +14,66 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// load XmlHttpRequest mock
import HttpBackend from "matrix-mock-request";
import "./setupTests";
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import type { MatrixClient, ClientEvent } from "../../src";
const USER_ID = "@user:test.server";
const DEVICE_ID = "device_id";
const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface Global {
matrixcs: {
MatrixClient: typeof MatrixClient;
ClientEvent: typeof ClientEvent;
};
}
}
}
describe("Browserify Test", function() {
let client;
let httpBackend;
let client: MatrixClient;
let httpBackend: HttpBackend;
beforeEach(() => {
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend = new HttpBackend();
client = new global.matrixcs.MatrixClient({
baseUrl: "http://test.server",
userId: USER_ID,
accessToken: ACCESS_TOKEN,
deviceId: DEVICE_ID,
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
client.startClient();
});
afterEach(async () => {
client.stopClient();
httpBackend.stop();
client.http.abort();
httpBackend.verifyNoOutstandingRequests();
httpBackend.verifyNoOutstandingExpectation();
await httpBackend.stop();
});
it("Sync", function() {
const event = utils.mkMembership({
room: ROOM_ID,
mship: "join",
user: "@other_user:server.test",
it("Sync", async () => {
const event = {
type: "m.room.member",
room_id: ROOM_ID,
content: {
membership: "join",
name: "Displayname",
});
},
event_id: "$foobar",
};
const syncData = {
next_batch: "batch1",
@ -71,11 +91,16 @@ describe("Browserify Test", function() {
};
httpBackend.when("GET", "/sync").respond(200, syncData);
return Promise.race([
httpBackend.flushAllExpected(),
new Promise((_, reject) => {
client.once("sync.unexpectedError", reject);
}),
]);
httpBackend.when("GET", "/sync").respond(200, syncData);
const syncPromise = new Promise(r => client.once(global.matrixcs.ClientEvent.Sync, r));
const unexpectedErrorFn = jest.fn();
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
client.startClient();
await httpBackend.flushAllExpected();
await syncPromise;
expect(unexpectedErrorFn).not.toHaveBeenCalled();
}, 20000); // additional timeout as this test can take quite a while
});

View File

@ -16,13 +16,12 @@ limitations under the License.
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils";
import { CRYPTO_ENABLED, MatrixClient, IStoredClientOpts } from "../../src/client";
import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
import { Filter, MemoryStore, Room } from "../../src/matrix";
import { Filter, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix";
import { TestClient } from "../TestClient";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IFilterDefinition } from "../../src/filter";
import { FileType } from "../../src/http-api";
import { ISearchResults } from "../../src/@types/search";
import { IStore } from "../../src/store";
@ -65,28 +64,27 @@ describe("MatrixClient", function() {
describe("uploadContent", function() {
const buf = Buffer.from('hello world');
const file = buf;
const opts = {
type: "text/plain",
name: "hi.txt",
};
it("should upload the file", function() {
httpBackend!.when(
"POST", "/_matrix/media/r0/upload",
).check(function(req) {
expect(req.rawData).toEqual(buf);
expect(req.queryParams?.filename).toEqual("hi.txt");
if (!(req.queryParams?.access_token == accessToken ||
req.headers["Authorization"] == "Bearer " + accessToken)) {
expect(true).toBe(false);
}
expect(req.headers["Authorization"]).toBe("Bearer " + accessToken);
expect(req.headers["Content-Type"]).toEqual("text/plain");
// @ts-ignore private property
expect(req.opts.json).toBeFalsy();
// @ts-ignore private property
expect(req.opts.timeout).toBe(undefined);
}).respond(200, "content", true);
}).respond(200, '{"content_uri": "content"}', true);
const prom = client!.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
} as unknown as FileType);
const prom = client!.uploadContent(file, opts);
expect(prom).toBeTruthy();
@ -96,8 +94,7 @@ describe("MatrixClient", function() {
expect(uploads[0].loaded).toEqual(0);
const prom2 = prom.then(function(response) {
// for backwards compatibility, we return the raw JSON
expect(response).toEqual("content");
expect(response.content_uri).toEqual("content");
const uploads = client!.getCurrentUploads();
expect(uploads.length).toEqual(0);
@ -107,28 +104,6 @@ describe("MatrixClient", function() {
return prom2;
});
it("should parse the response if rawResponse=false", function() {
httpBackend!.when(
"POST", "/_matrix/media/r0/upload",
).check(function(req) {
// @ts-ignore private property
expect(req.opts.json).toBeFalsy();
}).respond(200, { "content_uri": "uri" });
const prom = client!.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
} as unknown as FileType, {
rawResponse: false,
}).then(function(response) {
expect(response.content_uri).toEqual("uri");
});
httpBackend!.flush('');
return prom;
});
it("should parse errors into a MatrixError", function() {
httpBackend!.when(
"POST", "/_matrix/media/r0/upload",
@ -141,11 +116,7 @@ describe("MatrixClient", function() {
"error": "broken",
});
const prom = client!.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
} as unknown as FileType).then(function(response) {
const prom = client!.uploadContent(file, opts).then(function(response) {
throw Error("request not failed");
}, function(error) {
expect(error.httpStatus).toEqual(400);
@ -157,30 +128,18 @@ describe("MatrixClient", function() {
return prom;
});
it("should return a promise which can be cancelled", function() {
const prom = client!.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
} as unknown as FileType);
it("should return a promise which can be cancelled", async () => {
const prom = client!.uploadContent(file, opts);
const uploads = client!.getCurrentUploads();
expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0);
const prom2 = prom.then(function(response) {
throw Error("request not aborted");
}, function(error) {
expect(error).toEqual("aborted");
const uploads = client!.getCurrentUploads();
expect(uploads.length).toEqual(0);
});
const r = client!.cancelUpload(prom);
expect(r).toBe(true);
return prom2;
await expect(prom).rejects.toThrow("Aborted");
expect(client.getCurrentUploads()).toHaveLength(0);
});
});
@ -202,6 +161,30 @@ describe("MatrixClient", function() {
client!.joinRoom(roomId);
httpBackend!.verifyNoOutstandingRequests();
});
it("should send request to inviteSignUrl if specified", async () => {
const roomId = "!roomId:server";
const inviteSignUrl = "https://id.server/sign/this/for/me";
const viaServers = ["a", "b", "c"];
const signature = {
sender: "sender",
mxid: "@sender:foo",
token: "token",
signatures: {},
};
httpBackend!.when("POST", inviteSignUrl).respond(200, signature);
httpBackend!.when("POST", "/join/" + encodeURIComponent(roomId)).check(request => {
expect(request.data.third_party_signed).toEqual(signature);
}).respond(200, { room_id: roomId });
const prom = client.joinRoom(roomId, {
inviteSignUrl,
viaServers,
});
await httpBackend!.flushAllExpected();
expect((await prom).roomId).toBe(roomId);
});
});
describe("getFilter", function() {
@ -676,7 +659,7 @@ describe("MatrixClient", function() {
// The vote event has been copied into the thread
const eventRefWithThreadId = withThreadId(
eventPollResponseReference, eventPollStartThreadRoot.getId());
expect(eventRefWithThreadId.threadId).toBeTruthy();
expect(eventRefWithThreadId.threadRootId).toBeTruthy();
expect(threaded).toEqual([
eventPollStartThreadRoot,
@ -1178,15 +1161,150 @@ describe("MatrixClient", function() {
expect(await prom).toStrictEqual(response);
});
});
describe("logout", () => {
it("should abort pending requests when called with stopClient=true", async () => {
httpBackend.when("POST", "/logout").respond(200, {});
const fn = jest.fn();
client.http.request(Method.Get, "/test").catch(fn);
client.logout(true);
await httpBackend.flush(undefined);
expect(fn).toHaveBeenCalled();
});
});
describe("sendHtmlEmote", () => {
it("should send valid html emote", async () => {
httpBackend.when("PUT", "/send").check(req => {
expect(req.data).toStrictEqual({
"msgtype": "m.emote",
"body": "Body",
"formatted_body": "<h1>Body</h1>",
"format": "org.matrix.custom.html",
"org.matrix.msc1767.message": expect.anything(),
});
}).respond(200, { event_id: "$foobar" });
const prom = client.sendHtmlEmote("!room:server", "Body", "<h1>Body</h1>");
await httpBackend.flush(undefined);
await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" });
});
});
describe("sendHtmlMessage", () => {
it("should send valid html message", async () => {
httpBackend.when("PUT", "/send").check(req => {
expect(req.data).toStrictEqual({
"msgtype": "m.text",
"body": "Body",
"formatted_body": "<h1>Body</h1>",
"format": "org.matrix.custom.html",
"org.matrix.msc1767.message": expect.anything(),
});
}).respond(200, { event_id: "$foobar" });
const prom = client.sendHtmlMessage("!room:server", "Body", "<h1>Body</h1>");
await httpBackend.flush(undefined);
await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" });
});
});
describe("forget", () => {
it("should remove from store by default", async () => {
const room = new Room("!roomId:server", client, userId);
client.store.storeRoom(room);
expect(client.store.getRooms()).toContain(room);
httpBackend.when("POST", "/forget").respond(200, {});
await Promise.all([
client.forget(room.roomId),
httpBackend.flushAllExpected(),
]);
expect(client.store.getRooms()).not.toContain(room);
});
});
describe("getCapabilities", () => {
it("should cache by default", async () => {
httpBackend!.when("GET", "/capabilities").respond(200, {
capabilities: {
"m.change_password": false,
},
});
const prom = httpBackend!.flushAllExpected();
const capabilities1 = await client!.getCapabilities();
const capabilities2 = await client!.getCapabilities();
await prom;
expect(capabilities1).toStrictEqual(capabilities2);
});
});
describe("getTerms", () => {
it("should return Identity Server terms", async () => {
httpBackend!.when("GET", "/terms").respond(200, { foo: "bar" });
const prom = client!.getTerms(SERVICE_TYPES.IS, "http://identity.server");
await httpBackend!.flushAllExpected();
await expect(prom).resolves.toEqual({ foo: "bar" });
});
it("should return Integrations Manager terms", async () => {
httpBackend!.when("GET", "/terms").respond(200, { foo: "bar" });
const prom = client!.getTerms(SERVICE_TYPES.IM, "http://im.server");
await httpBackend!.flushAllExpected();
await expect(prom).resolves.toEqual({ foo: "bar" });
});
});
describe("publicRooms", () => {
it("should use GET request if no server or filter is specified", () => {
httpBackend!.when("GET", "/publicRooms").respond(200, {});
client!.publicRooms({});
return httpBackend!.flushAllExpected();
});
it("should use GET request if only server is specified", () => {
httpBackend!.when("GET", "/publicRooms").check(request => {
expect(request.queryParams.server).toBe("server1");
}).respond(200, {});
client!.publicRooms({ server: "server1" });
return httpBackend!.flushAllExpected();
});
it("should use POST request if filter is specified", () => {
httpBackend!.when("POST", "/publicRooms").check(request => {
expect(request.data.filter.generic_search_term).toBe("foobar");
}).respond(200, {});
client!.publicRooms({ filter: { generic_search_term: "foobar" } });
return httpBackend!.flushAllExpected();
});
});
describe("login", () => {
it("should persist values to the client opts", async () => {
const token = "!token&";
const userId = "@m:t";
httpBackend!.when("POST", "/login").respond(200, {
access_token: token,
user_id: userId,
});
const prom = client!.login("fake.login", {});
await httpBackend!.flushAllExpected();
const resp = await prom;
expect(resp.access_token).toBe(token);
expect(resp.user_id).toBe(userId);
expect(client.getUserId()).toBe(userId);
expect(client.http.opts.accessToken).toBe(token);
});
});
});
function withThreadId(event, newThreadId) {
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
const ret = event.toSnapshot();
ret.setThreadId(newThreadId);
return ret;
}
const buildEventMessageInThread = (root) => new MatrixEvent({
const buildEventMessageInThread = (root: MatrixEvent) => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
@ -1233,7 +1351,7 @@ const buildEventPollResponseReference = () => new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventReaction = (event) => new MatrixEvent({
const buildEventReaction = (event: MatrixEvent) => new MatrixEvent({
"content": {
"m.relates_to": {
"event_id": event.getId(),
@ -1252,7 +1370,7 @@ const buildEventReaction = (event) => new MatrixEvent({
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
});
const buildEventRedaction = (event) => new MatrixEvent({
const buildEventRedaction = (event: MatrixEvent) => new MatrixEvent({
"content": {
},
@ -1286,7 +1404,7 @@ const buildEventPollStartThreadRoot = () => new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventReply = (target) => new MatrixEvent({
const buildEventReply = (target: MatrixEvent) => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
@ -1452,7 +1570,7 @@ const buildEventCreate = () => new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
function assertObjectContains(obj, expected) {
function assertObjectContains(obj: object, expected: any): void {
for (const k in expected) {
if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]);

View File

@ -5,7 +5,6 @@ import { MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api";
import { ICreateClientOpts } from "../../src/client";
import { IStore } from "../../src/store";
describe("MatrixClient opts", function() {
@ -69,7 +68,7 @@ describe("MatrixClient opts", function() {
let client;
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn as unknown as ICreateClientOpts['request'],
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: undefined,
baseUrl: baseUrl,
userId: userId,
@ -129,7 +128,7 @@ describe("MatrixClient opts", function() {
let client;
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn as unknown as ICreateClientOpts['request'],
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: new MemoryStore() as IStore,
baseUrl: baseUrl,
userId: userId,
@ -143,7 +142,7 @@ describe("MatrixClient opts", function() {
});
it("shouldn't retry sending events", function(done) {
httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({
httpBackend.when("PUT", "/txn1").respond(500, new MatrixError({
errcode: "M_SOMETHING",
error: "Ruh roh",
}));

View File

@ -18,7 +18,7 @@ import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event";
import { ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
import { TestClient } from "../TestClient";
describe("MatrixClient room timelines", function() {
@ -802,17 +802,14 @@ describe("MatrixClient room timelines", function() {
it('Timeline recovers after `/context` request to generate new timeline fails', async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from.
httpBackend!.when("GET", contextUrl)
.respond(500, function() {
httpBackend!.when("GET", contextUrl).check(() => {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
return {
}).respond(500, new MatrixError({
errcode: 'TEST_FAKE_ERROR',
error: 'We purposely intercepted this /context request to make it fail ' +
'in order to test whether the refresh timeline code is resilient',
};
});
}));
// Refresh the timeline and expect it to fail
const settledFailedRefreshPromises = await Promise.allSettled([

View File

@ -1572,7 +1572,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
const idbHttpBackend = idbTestClient.httpBackend;
const idbClient = idbTestClient.client;
idbHttpBackend.when("GET", "/versions").respond(200, {});
idbHttpBackend.when("GET", "/pushrules").respond(200, {});
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
await idbClient.initCrypto();

View File

@ -23,12 +23,13 @@ import { TestClient } from "../TestClient";
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
import {
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError,
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent,
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent,
} from "../../src";
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src/client";
import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined;
@ -530,6 +531,7 @@ describe("SlidingSyncSdk", () => {
],
});
await httpBackend!.flush("/profile", 1, 1000);
await emitPromise(client!, RoomMemberEvent.Name);
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
const inviteeMember = room.getMember(invitee)!;

19
spec/setupTests.ts Normal file
View File

@ -0,0 +1,19 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import DOMException from "domexception";
global.DOMException = DOMException;

View File

@ -17,13 +17,12 @@ limitations under the License.
import MockHttpBackend from "matrix-mock-request";
import { request } from "../../src/matrix";
import { AutoDiscovery } from "../../src/autodiscovery";
describe("AutoDiscovery", function() {
const getHttpBackend = (): MockHttpBackend => {
const httpBackend = new MockHttpBackend();
request(httpBackend.requestFn);
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
return httpBackend;
};
@ -176,8 +175,7 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (empty string)", function() {
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (empty string)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@ -205,8 +203,7 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (no property)", function() {
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (no property)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {},
@ -232,8 +229,7 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (disallowed scheme)", function() {
it("should return FAIL_ERROR when .well-known has an invalid base_url for m.homeserver (disallowed scheme)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@ -679,4 +675,76 @@ describe("AutoDiscovery", function() {
}),
]);
});
it("should return FAIL_PROMPT for connection errors", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined);
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT for fetch errors", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, new Error("CORS or something"));
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT for invalid JSON", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "<html>", true);
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
});

View File

@ -30,7 +30,7 @@ import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup";
import { StubStore } from "../../../src/store/stub";
import { IAbortablePromise, MatrixScheduler } from '../../../src';
import { MatrixScheduler } from '../../../src';
const Olm = global.Olm;
@ -131,7 +131,7 @@ function makeTestClient(cryptoStore) {
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: jest.fn(), // NOP
fetchFn: jest.fn(), // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
@ -197,7 +197,7 @@ describe("MegolmBackup", function() {
// to tick the clock between the first try and the retry.
const realSetTimeout = global.setTimeout;
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) {
return realSetTimeout(f, n/100);
return realSetTimeout(f!, n/100);
});
});
@ -298,25 +298,25 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
};
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@ -381,25 +381,25 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
};
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@ -439,7 +439,7 @@ describe("MegolmBackup", function() {
new Promise<void>((resolve, reject) => {
let backupInfo;
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
@ -449,23 +449,23 @@ describe("MegolmBackup", function() {
try {
// make sure auth_data is signed by the master key
olmlib.pkVerify(
data.auth_data, client.getCrossSigningId(), "@alice:bar",
(data as Record<string, any>).auth_data, client.getCrossSigningId(), "@alice:bar",
);
} catch (e) {
reject(e);
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({});
}
backupInfo = data;
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({});
} else if (numCalls === 2) {
expect(method).toBe("GET");
expect(path).toBe("/room_keys/version");
resolve();
return Promise.resolve(backupInfo) as IAbortablePromise<any>;
return Promise.resolve(backupInfo);
} else {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many times"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({});
}
};
}),
@ -495,7 +495,7 @@ describe("MegolmBackup", function() {
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: jest.fn(), // NOP
fetchFn: jest.fn(), // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
@ -542,30 +542,30 @@ describe("MegolmBackup", function() {
let numCalls = 0;
await new Promise<void>((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls >= 3) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
if (numCalls > 1) {
resolve();
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
} else {
return Promise.reject(
new Error("this is an expected failure"),
) as IAbortablePromise<any>;
);
}
};
return client.crypto.backupManager.backupGroupSession(

View File

@ -141,7 +141,7 @@ describe("Cross Signing", function() {
};
alice.uploadKeySignatures = async () => ({ failures: {} });
alice.setAccountData = async () => ({});
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T> => ({} as T);
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T);
const authUploadDeviceSigningKeys = async func => await func({});
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass

View File

@ -109,16 +109,13 @@ describe("Secrets", function() {
const secretStorage = alice.crypto.secretStorage;
jest.spyOn(alice, 'setAccountData').mockImplementation(
async function(eventType, contents, callback) {
async function(eventType, contents) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
content: contents,
}),
]);
if (callback) {
callback(undefined, undefined);
}
return {};
});
@ -192,7 +189,7 @@ describe("Secrets", function() {
},
},
);
alice.setAccountData = async function(eventType, contents, callback) {
alice.setAccountData = async function(eventType, contents) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
@ -332,7 +329,7 @@ describe("Secrets", function() {
);
bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined);
bob.setAccountData = async function(eventType, contents, callback) {
bob.setAccountData = async function(eventType, contents) {
const event = new MatrixEvent({
type: eventType,
content: contents,

View File

@ -29,7 +29,7 @@ describe("eventMapperFor", function() {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: function() {} as any, // NOP
fetchFn: function() {} as any, // NOP
store: {
getRoom(roomId: string): Room | null {
return rooms.find(r => r.roomId === roomId);

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = `
{
"base": "http://baseUrl",
"params": {
"access_token": "token",
},
"path": "/_matrix/media/r0/upload",
}
`;

View File

@ -0,0 +1,223 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FetchHttpApi } from "../../../src/http-api/fetch";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
describe("FetchHttpApi", () => {
const baseUrl = "http://baseUrl";
const idBaseUrl = "http://idBaseUrl";
const prefix = ClientPrefix.V3;
it("should support aborting multiple times", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
api.request(Method.Get, "/foo");
api.request(Method.Get, "/baz");
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy();
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
api.request(Method.Get, "/bar");
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy();
});
it("should fall back to global fetch if fetchFn not provided", () => {
global.fetch = jest.fn();
expect(global.fetch).not.toHaveBeenCalled();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
api.fetch("test");
expect(global.fetch).toHaveBeenCalled();
});
it("should update identity server base url", () => {
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
expect(api.opts.idBaseUrl).toBeUndefined();
api.setIdBaseUrl("https://id.foo.bar");
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
});
describe("idServerRequest", () => {
it("should throw if no idBaseUrl", () => {
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2))
.toThrow("No identity server base URL set");
});
it("should send params as query string for GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
});
it("should send params as body for non-GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
const params = { foo: "bar", via: ["a", "b"] };
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
});
it("should add Authorization header if token provided", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
});
});
it("should return the Response object if onlyData=false", async () => {
const res = { ok: true };
const fetchFn = jest.fn().mockResolvedValue(res);
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: false });
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res);
});
it("should return text if json=false", async () => {
const text = "418 I'm a teapot";
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(api.requestOtherUrl(Method.Get, "http://url", undefined, {
json: false,
})).resolves.toBe(text);
});
it("should send token via query params if useAuthorizationHeader=false", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: false,
});
api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
});
it("should send token via headers by default", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
});
api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
});
it("should not send a token if not calling `authedRequest`", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
});
api.request(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy();
});
it("should ensure no token is leaked out via query params if sending via headers", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: true,
});
api.authedRequest(Method.Get, "/path", { access_token: "123" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
});
it("should not override manually specified access token via query params", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: false,
});
api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
});
it("should not override manually specified access token via header", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: true,
});
api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Authorization: "Bearer RealToken" },
});
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken");
});
it("should not override Accept header", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Accept: "text/html" },
});
expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html");
});
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
const fetchFn = jest.fn().mockResolvedValue({
ok: false,
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
text: jest.fn().mockResolvedValue(JSON.stringify({
errcode: "M_CONSENT_NOT_GIVEN",
error: "Ye shall ask for consent",
})),
});
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
await Promise.all([
emitPromise(emitter, HttpApiEvent.NoConsent),
expect(api.authedRequest(Method.Get, "/path")).rejects.toThrow("Ye shall ask for consent"),
]);
});
});

View File

@ -0,0 +1,228 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import DOMException from "domexception";
import { mocked } from "jest-mock";
import { ClientPrefix, MatrixHttpApi, Method, UploadResponse } from "../../../src";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
jest.useFakeTimers();
describe("MatrixHttpApi", () => {
const baseUrl = "http://baseUrl";
const prefix = ClientPrefix.V3;
let xhr: Partial<Writeable<XMLHttpRequest>>;
let upload: Promise<UploadResponse>;
const DONE = 0;
global.DOMException = DOMException;
beforeEach(() => {
xhr = {
upload: {} as XMLHttpRequestUpload,
open: jest.fn(),
send: jest.fn(),
abort: jest.fn(),
setRequestHeader: jest.fn(),
onreadystatechange: undefined,
getResponseHeader: jest.fn(),
};
// We stub out XHR here as it is not available in JSDOM
// @ts-ignore
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
// @ts-ignore
global.XMLHttpRequest.DONE = DONE;
});
afterEach(() => {
upload?.catch(() => {});
// Abort any remaining requests
xhr.readyState = DONE;
xhr.status = 0;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
});
it("should fall back to `fetch` where xhr is unavailable", () => {
global.XMLHttpRequest = undefined;
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
upload = api.uploadContent({} as File);
expect(fetchFn).toHaveBeenCalled();
});
it("should prefer xhr where available", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
upload = api.uploadContent({} as File);
expect(fetchFn).not.toHaveBeenCalled();
expect(xhr.open).toHaveBeenCalled();
});
it("should send access token in query params if header disabled", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
accessToken: "token",
useAuthorizationHeader: false,
});
upload = api.uploadContent({} as File);
expect(xhr.open)
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token");
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
});
it("should send access token in header by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
accessToken: "token",
});
upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
});
it("should include filename by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File, { name: "name" });
expect(xhr.open)
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name");
});
it("should allow not sending the filename", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
});
it("should abort xhr when the upload is aborted", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
api.cancelUpload(upload);
expect(xhr.abort).toHaveBeenCalled();
return expect(upload).rejects.toThrow("Aborted");
});
it("should timeout if no progress in 30s", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
jest.advanceTimersByTime(25000);
// @ts-ignore
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
jest.advanceTimersByTime(25000);
expect(xhr.abort).not.toHaveBeenCalled();
jest.advanceTimersByTime(5000);
expect(xhr.abort).toHaveBeenCalled();
});
it("should call progressHandler", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
const progressHandler = jest.fn();
upload = api.uploadContent({} as File, { progressHandler });
const progressEvent = new Event("progress") as ProgressEvent;
Object.assign(progressEvent, { loaded: 1, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
Object.assign(progressEvent, { loaded: 95, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
});
it("should error when no response body", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = "";
xhr.status = 200;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("No response body.");
});
it("should error on a 400-code", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
xhr.status = 404;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("Not found");
});
it("should return response on successful upload", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"content_uri": "mxc://server/foobar"}';
xhr.status = 200;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
});
it("should abort xhr when calling `cancelUpload`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
expect(api.cancelUpload(upload)).toBeTruthy();
expect(xhr.abort).toHaveBeenCalled();
});
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.status = 500;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
await upload.catch(() => {});
expect(api.cancelUpload(upload)).toBeFalsy();
expect(xhr.abort).not.toHaveBeenCalled();
});
it("should return active uploads in `getCurrentUploads`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeTruthy();
api.cancelUpload(upload);
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeFalsy();
});
it("should return expected object from `getContentUri`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, accessToken: "token" });
expect(api.getContentUri()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,183 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import {
anySignal,
ConnectionError,
MatrixError,
parseErrorResponse,
retryNetworkOperation,
timeoutSignal,
} from "../../../src";
import { sleep } from "../../../src/utils";
jest.mock("../../../src/utils");
describe("timeoutSignal", () => {
jest.useFakeTimers();
it("should fire abort signal after specified timeout", () => {
const signal = timeoutSignal(3000);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(3000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
});
describe("anySignal", () => {
jest.useFakeTimers();
it("should fire when any signal fires", () => {
const { signal } = anySignal([
timeoutSignal(3000),
timeoutSignal(2000),
]);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(2000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
it("should cleanup when instructed", () => {
const { signal, cleanup } = anySignal([
timeoutSignal(3000),
timeoutSignal(2000),
]);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
cleanup();
jest.advanceTimersByTime(2000);
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
});
it("should abort immediately if passed an aborted signal", () => {
const controller = new AbortController();
controller.abort();
const { signal } = anySignal([controller.signal]);
expect(signal.aborted).toBeTruthy();
});
});
describe("parseErrorResponse", () => {
it("should resolve Matrix Errors from XHR", () => {
expect(parseErrorResponse({
getResponseHeader(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
status: 500,
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500));
});
it("should resolve Matrix Errors from fetch", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500));
});
it("should handle no type gracefully", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new Error("Server returned 500 error"));
});
it("should handle invalid type gracefully", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? " " : null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}'))
.toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type"));
});
it("should handle plaintext errors", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "text/plain" : null;
},
},
status: 418,
} as Response, "I'm a teapot")).toStrictEqual(new Error("Server returned 418 error: I'm a teapot"));
});
});
describe("retryNetworkOperation", () => {
it("should retry given number of times with exponential sleeps", async () => {
const err = new ConnectionError("test");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(4);
expect(mocked(sleep)).toHaveBeenCalledTimes(3);
expect(mocked(sleep).mock.calls[0][0]).toBe(2000);
expect(mocked(sleep).mock.calls[1][0]).toBe(4000);
expect(mocked(sleep).mock.calls[2][0]).toBe(8000);
});
it("should bail out on errors other than ConnectionError", async () => {
const err = new TypeError("invalid JSON");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(1);
});
it("should return newest ConnectionError when giving up", async () => {
const err1 = new ConnectionError("test1");
const err2 = new ConnectionError("test2");
const err3 = new ConnectionError("test3");
const errors = [err1, err2, err3];
const fn = jest.fn().mockImplementation(() => {
throw errors.shift();
});
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
});
});

View File

@ -103,7 +103,7 @@ describe("MatrixClient", function() {
];
let acceptKeepalives: boolean;
let pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
function httpReq(method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return Promise.resolve({
unstable_features: {
@ -132,7 +132,6 @@ describe("MatrixClient", function() {
method: method,
path: path,
};
pendingLookup.promise.abort = () => {}; // to make it a valid IAbortablePromise
return pendingLookup.promise;
}
if (next.path === path && next.method === method) {
@ -178,7 +177,7 @@ describe("MatrixClient", function() {
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
accessToken: "my.access.token",
request: function() {} as any, // NOP
fetchFn: function() {} as any, // NOP
store: store,
scheduler: scheduler,
userId: userId,
@ -1153,8 +1152,7 @@ describe("MatrixClient", function() {
// event type combined
const expectedEventType = M_BEACON_INFO.name;
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(callback).toBeFalsy();
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(method).toBe('PUT');
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
@ -1168,7 +1166,7 @@ describe("MatrixClient", function() {
await client.unstable_setLiveBeacon(roomId, content);
// event type combined
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
const [, path, , requestContent] = client.http.authedRequest.mock.calls[0];
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
@ -1229,7 +1227,7 @@ describe("MatrixClient", function() {
it("is called with plain text topic and callback and sends state event", async () => {
const sendStateEvent = createSendStateEventMock("pizza");
client.sendStateEvent = sendStateEvent;
await client.setRoomTopic(roomId, "pizza", () => {});
await client.setRoomTopic(roomId, "pizza");
expect(sendStateEvent).toHaveBeenCalledTimes(1);
});
@ -1244,15 +1242,9 @@ describe("MatrixClient", function() {
describe("setPassword", () => {
const auth = { session: 'abcdef', type: 'foo' };
const newPassword = 'newpassword';
const callback = () => {};
const passwordTest = (expectedRequestContent: any, expectedCallback?: Function) => {
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
if (expectedCallback) {
expect(callback).toBe(expectedCallback);
} else {
expect(callback).toBeFalsy();
}
const passwordTest = (expectedRequestContent: any) => {
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(method).toBe('POST');
expect(path).toEqual('/account/password');
expect(queryParams).toBeFalsy();
@ -1269,8 +1261,8 @@ describe("MatrixClient", function() {
});
it("no logout_devices specified + callback", async () => {
await client.setPassword(auth, newPassword, callback);
passwordTest({ auth, new_password: newPassword }, callback);
await client.setPassword(auth, newPassword);
passwordTest({ auth, new_password: newPassword });
});
it("overload logoutDevices=true", async () => {
@ -1279,8 +1271,8 @@ describe("MatrixClient", function() {
});
it("overload logoutDevices=true + callback", async () => {
await client.setPassword(auth, newPassword, true, callback);
passwordTest({ auth, new_password: newPassword, logout_devices: true }, callback);
await client.setPassword(auth, newPassword, true);
passwordTest({ auth, new_password: newPassword, logout_devices: true });
});
it("overload logoutDevices=false", async () => {
@ -1289,8 +1281,8 @@ describe("MatrixClient", function() {
});
it("overload logoutDevices=false + callback", async () => {
await client.setPassword(auth, newPassword, false, callback);
passwordTest({ auth, new_password: newPassword, logout_devices: false }, callback);
await client.setPassword(auth, newPassword, false);
passwordTest({ auth, new_password: newPassword, logout_devices: false });
});
});
@ -1305,8 +1297,7 @@ describe("MatrixClient", function() {
const result = await client.getLocalAliases(roomId);
// Current version of the endpoint we support is v3
const [callback, method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0];
expect(callback).toBeFalsy();
const [method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0];
expect(data).toBeFalsy();
expect(method).toBe('GET');
expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`);

View File

@ -890,9 +890,8 @@ describe("MSC3089TreeSpace", () => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
});
return Promise.resolve(mxc);
return Promise.resolve({ content_uri: mxc });
});
client.uploadContent = uploadFn;
@ -950,9 +949,8 @@ describe("MSC3089TreeSpace", () => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
});
return Promise.resolve(mxc);
return Promise.resolve({ content_uri: mxc });
});
client.uploadContent = uploadFn;

View File

@ -16,7 +16,7 @@ limitations under the License.
import MockHttpBackend from 'matrix-mock-request';
import { IHttpOpts, MatrixClient, PUSHER_ENABLED } from "../../src/matrix";
import { MatrixClient, PUSHER_ENABLED } from "../../src/matrix";
import { mkPusher } from '../test-utils/test-utils';
const realSetTimeout = setTimeout;
@ -35,7 +35,7 @@ describe("Pushers", () => {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: httpBackend.requestFn as unknown as IHttpOpts["request"],
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
});

View File

@ -17,7 +17,7 @@ limitations under the License.
import MockHttpBackend from 'matrix-mock-request';
import { indexedDB as fakeIndexedDB } from 'fake-indexeddb';
import { IHttpOpts, IndexedDBStore, MatrixEvent, MemoryStore, Room } from "../../src";
import { IndexedDBStore, MatrixEvent, MemoryStore, Room } from "../../src";
import { MatrixClient } from "../../src/client";
import { ToDeviceBatch } from '../../src/models/ToDeviceMessage';
import { logger } from '../../src/logger';
@ -89,7 +89,7 @@ describe.each([
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: httpBackend.requestFn as IHttpOpts["request"],
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store,
});
});

View File

@ -18,7 +18,6 @@ import MockHttpBackend from 'matrix-mock-request';
import { ReceiptType } from '../../src/@types/read_receipts';
import { MatrixClient } from "../../src/client";
import { IHttpOpts } from '../../src/http-api';
import { EventType } from '../../src/matrix';
import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt';
import { encodeUri } from '../../src/utils';
@ -87,7 +86,7 @@ describe("Read receipt", () => {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: httpBackend.requestFn as unknown as IHttpOpts["request"],
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
client.isGuest = () => false;
});
@ -146,5 +145,23 @@ describe("Read receipt", () => {
await httpBackend.flushAllExpected();
await flushPromises();
});
it("sends a valid room read receipt even when body omitted", async () => {
httpBackend.when(
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId(),
}),
).check((request) => {
expect(request.data).toEqual({});
}).respond(200, {});
mockServerSideSupport(client, false);
client.sendReceipt(threadEvent, ReceiptType.Read, undefined);
await httpBackend.flushAllExpected();
await flushPromises();
});
});
});

View File

@ -26,9 +26,7 @@ describe("utils", function() {
foo: "bar",
baz: "beer@",
};
expect(utils.encodeParams(params)).toEqual(
"foo=bar&baz=beer%40",
);
expect(utils.encodeParams(params).toString()).toEqual("foo=bar&baz=beer%40");
});
it("should handle boolean and numeric values", function() {
@ -37,7 +35,24 @@ describe("utils", function() {
number: 12345,
boolean: false,
};
expect(utils.encodeParams(params)).toEqual("string=foobar&number=12345&boolean=false");
expect(utils.encodeParams(params).toString()).toEqual("string=foobar&number=12345&boolean=false");
});
it("should handle string arrays", () => {
const params = {
via: ["one", "two", "three"],
};
expect(utils.encodeParams(params).toString()).toEqual("via=one&via=two&via=three");
});
});
describe("decodeParams", () => {
it("should be able to decode multiple values into an array", () => {
const params = "foo=bar&via=a&via=b&via=c";
expect(utils.decodeParams(params)).toEqual({
foo: "bar",
via: ["a", "b", "c"],
});
});
});

View File

@ -30,6 +30,8 @@ declare global {
namespace NodeJS {
interface Global {
localStorage: Storage;
// marker variable used to detect both the browser & node entrypoints being used at once
__js_sdk_entrypoint: unknown;
}
}

View File

@ -40,11 +40,6 @@ export enum Preset {
export type ResizeMethod = "crop" | "scale";
// TODO move to http-api after TSification
export interface IAbortablePromise<T> extends Promise<T> {
abort(): void;
}
export type IdServerUnbindResult = "no-support" | "success";
// Knock and private are reserved keywords which are not yet implemented.

View File

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Callback } from "../client";
import { IContent, IEvent } from "../models/event";
import { Preset, Visibility } from "./partials";
import { IEventWithRoomId, SearchKey } from "./search";
@ -130,16 +129,6 @@ export interface IRoomDirectoryOptions {
third_party_instance_id?: string;
}
export interface IUploadOpts {
name?: string;
includeFilename?: boolean;
type?: string;
rawResponse?: boolean;
onlyContentUri?: boolean;
callback?: Callback;
progressHandler?: (state: {loaded: number, total: number}) => void;
}
export interface IAddThreePidOnlyBody {
auth?: {
type: string;

View File

@ -28,7 +28,7 @@ const MAX_BATCH_SIZE = 20;
export class ToDeviceMessageQueue {
private sending = false;
private running = true;
private retryTimeout: number = null;
private retryTimeout: ReturnType<typeof setTimeout> | null = null;
private retryAttempts = 0;
constructor(private client: MatrixClient) {
@ -68,7 +68,7 @@ export class ToDeviceMessageQueue {
logger.debug("Attempting to send queued to-device messages");
this.sending = true;
let headBatch;
let headBatch: IndexedToDeviceBatch;
try {
while (this.running) {
headBatch = await this.client.store.getOldestToDeviceBatch();
@ -92,7 +92,7 @@ export class ToDeviceMessageQueue {
// bored and giving up for now
if (Math.floor(e.httpStatus / 100) === 4) {
logger.error("Fatal error when sending to-device message - dropping to-device batch!", e);
await this.client.store.removeToDeviceBatch(headBatch.id);
await this.client.store.removeToDeviceBatch(headBatch!.id);
} else {
logger.info("Automatic retry limit reached for to-device messages.");
}

View File

@ -17,10 +17,9 @@ limitations under the License.
/** @module auto-discovery */
import { ServerResponse } from "http";
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger';
import { MatrixError, Method, timeoutSignal } from "./http-api";
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
@ -395,6 +394,19 @@ export class AutoDiscovery {
}
}
private static fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
if (this.fetchFn) {
return this.fetchFn(resource, options);
}
return global.fetch(resource, options);
}
private static fetchFn?: typeof global.fetch;
public static setFetchFn(fetchFn: typeof global.fetch): void {
AutoDiscovery.fetchFn = fetchFn;
}
/**
* Fetches a JSON object from a given URL, as expected by all .well-known
* related lookups. If the server gives a 404 then the `action` will be
@ -411,45 +423,55 @@ export class AutoDiscovery {
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
private static fetchWellKnownObject(uri: string): Promise<IWellKnownConfig> {
return new Promise((resolve) => {
// eslint-disable-next-line
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
{ method: "GET", uri, timeout: 5000 },
(error: Error, response: ServerResponse, body: string) => {
if (error || response?.statusCode < 200 || response?.statusCode >= 300) {
const result = { error, raw: {} };
return resolve(response?.statusCode === 404
? {
...result,
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
let response: Response;
try {
response = await AutoDiscovery.fetch(url, {
method: Method.Get,
signal: timeoutSignal(5000),
});
if (response.status === 404) {
return {
raw: {},
action: AutoDiscoveryAction.IGNORE,
reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN,
} : {
...result,
};
}
if (!response.ok) {
return {
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: error?.message || "General failure",
});
reason: "General failure",
};
}
} catch (err) {
const error = err as Error | string | undefined;
return {
error,
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: (<Error>error)?.message || "General failure",
};
}
try {
return resolve({
raw: JSON.parse(body),
return {
raw: await response.json(),
action: AutoDiscoveryAction.SUCCESS,
});
};
} catch (err) {
return resolve({
error: err,
const error = err as Error | string | undefined;
return {
error,
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: err?.name === "SyntaxError"
reason: (error as MatrixError)?.name === "SyntaxError"
? AutoDiscovery.ERROR_INVALID_JSON
: AutoDiscovery.ERROR_INVALID,
});
};
}
},
);
});
}
}

View File

@ -14,25 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import request from "browser-request";
import queryString from "qs";
import * as matrixcs from "./matrix";
if (matrixcs.getRequest()) {
if (global.__js_sdk_entrypoint) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
}
matrixcs.request(function(opts, fn) {
// We manually fix the query string for browser-request because
// it doesn't correctly handle cases like ?via=one&via=two. Instead
// we mimic `request`'s query string interface to make it all work
// as expected.
// browser-request will happily take the constructed string as the
// query string without trying to modify it further.
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
return request(opts, fn);
});
global.__js_sdk_entrypoint = true;
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ import { logger } from "../logger";
import { MatrixEvent } from "../models/event";
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { Method, PREFIX_V3 } from "../http-api";
import { Method, ClientPrefix } from "../http-api";
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
import {
ClientEvent,
@ -241,19 +241,19 @@ export class EncryptionSetupOperation {
// Sign the backup with the cross signing key so the key backup can
// be trusted via cross-signing.
await baseApis.http.authedRequest(
undefined, Method.Put, "/room_keys/version/" + this.keyBackupInfo.version,
Method.Put, "/room_keys/version/" + this.keyBackupInfo.version,
undefined, {
algorithm: this.keyBackupInfo.algorithm,
auth_data: this.keyBackupInfo.auth_data,
},
{ prefix: PREFIX_V3 },
{ prefix: ClientPrefix.V3 },
);
} else {
// add new key backup
await baseApis.http.authedRequest(
undefined, Method.Post, "/room_keys/version",
Method.Post, "/room_keys/version",
undefined, this.keyBackupInfo,
{ prefix: PREFIX_V3 },
{ prefix: ClientPrefix.V3 },
);
}
}

View File

@ -210,7 +210,6 @@ export class DehydrationManager {
logger.log("Uploading account to server");
// eslint-disable-next-line camelcase
const dehydrateResult = await this.crypto.baseApis.http.authedRequest<{ device_id: string }>(
undefined,
Method.Put,
"/dehydrated_device",
undefined,
@ -273,7 +272,6 @@ export class DehydrationManager {
logger.log("Uploading keys to server");
await this.crypto.baseApis.http.authedRequest(
undefined,
Method.Post,
"/keys/upload/" + encodeURI(deviceId),
undefined,

File diff suppressed because it is too large Load Diff

64
src/http-api/errors.ts Normal file
View File

@ -0,0 +1,64 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IUsageLimit } from "../@types/partials";
interface IErrorJson extends Partial<IUsageLimit> {
[key: string]: any; // extensible
errcode?: string;
error?: string;
}
/**
* Construct a Matrix error. This is a JavaScript Error with additional
* information specific to the standard Matrix error response.
* @constructor
* @param {Object} errorJson The Matrix error JSON returned from the homeserver.
* @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
* @prop {string} name Same as MatrixError.errcode but with a default unknown string.
* @prop {string} message The Matrix 'error' value, e.g. "Missing token."
* @prop {Object} data The raw Matrix error JSON used to construct this object.
* @prop {number} httpStatus The numeric HTTP status code given
*/
export class MatrixError extends Error {
public readonly errcode?: string;
public readonly data: IErrorJson;
constructor(errorJson: IErrorJson = {}, public httpStatus?: number) {
super(`MatrixError: ${errorJson.errcode}`);
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.message = errorJson.error || "Unknown message";
this.data = errorJson;
}
}
/**
* Construct a ConnectionError. This is a JavaScript Error indicating
* that a request failed because of some error with the connection, either
* CORS was not correctly configured on the server, the server didn't response,
* the request timed out, or the internet connection on the client side went down.
* @constructor
*/
export class ConnectionError extends Error {
constructor(message: string, cause?: Error) {
super(message + (cause ? `: ${cause.message}` : ""));
}
get name() {
return "ConnectionError";
}
}

327
src/http-api/fetch.ts Normal file
View File

@ -0,0 +1,327 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* This is an internal module. See {@link MatrixHttpApi} for the public class.
* @module http-api
*/
import * as utils from "../utils";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { Method } from "./method";
import { ConnectionError, MatrixError } from "./errors";
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IRequestOpts } from "./interface";
import { anySignal, parseErrorResponse, timeoutSignal } from "./utils";
import { QueryDict } from "../utils";
type Body = Record<string, any> | BodyInit;
interface TypedResponse<T> extends Response {
json(): Promise<T>;
}
export type ResponseType<T, O extends IHttpOpts> =
O extends undefined ? T :
O extends { onlyData: true } ? T :
TypedResponse<T>;
export class FetchHttpApi<O extends IHttpOpts> {
private abortController = new AbortController();
constructor(
private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
public readonly opts: O,
) {
utils.checkObjectHasKeys(opts, ["baseUrl", "prefix"]);
opts.onlyData = !!opts.onlyData;
opts.useAuthorizationHeader = opts.useAuthorizationHeader ?? true;
}
public abort(): void {
this.abortController.abort();
this.abortController = new AbortController();
}
public fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
if (this.opts.fetchFn) {
return this.opts.fetchFn(resource, options);
}
return global.fetch(resource, options);
}
/**
* Sets the base URL for the identity server
* @param {string} url The new base url
*/
public setIdBaseUrl(url: string): void {
this.opts.idBaseUrl = url;
}
public idServerRequest<T extends {}>(
method: Method,
path: string,
params: Record<string, string | string[]>,
prefix: string,
accessToken?: string,
): Promise<ResponseType<T, O>> {
if (!this.opts.idBaseUrl) {
throw new Error("No identity server base URL set");
}
let queryParams: QueryDict | undefined = undefined;
let body: Record<string, string | string[]> | undefined = undefined;
if (method === Method.Get) {
queryParams = params;
} else {
body = params;
}
const fullUri = this.getUrl(path, queryParams, prefix, this.opts.idBaseUrl);
const opts: IRequestOpts = {
json: true,
headers: {},
};
if (accessToken) {
opts.headers.Authorization = `Bearer ${accessToken}`;
}
return this.requestOtherUrl(method, fullUri, body, opts);
}
/**
* Perform an authorised request to the homeserver.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} [body] The HTTP JSON body.
*
* @param {Object|Number=} opts additional options. If a number is specified,
* this is treated as `opts.localTimeoutMs`.
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {string=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {string=} opts.baseUrl The alternative base url to use.
* If not specified, uses this.opts.baseUrl
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
public authedRequest<T>(
method: Method,
path: string,
queryParams?: QueryDict,
body?: Body,
opts: IRequestOpts = {},
): Promise<ResponseType<T, O>> {
if (!queryParams) queryParams = {};
if (this.opts.useAuthorizationHeader) {
if (!opts.headers) {
opts.headers = {};
}
if (!opts.headers.Authorization) {
opts.headers.Authorization = "Bearer " + this.opts.accessToken;
}
if (queryParams.access_token) {
delete queryParams.access_token;
}
} else if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
const requestPromise = this.request<T>(method, path, queryParams, body, opts);
requestPromise.catch((err: MatrixError) => {
if (err.errcode == 'M_UNKNOWN_TOKEN' && !opts?.inhibitLogoutEmit) {
this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err);
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri);
}
});
// return the original promise, otherwise tests break due to it having to
// go around the event loop one more time to process the result of the request
return requestPromise;
}
/**
* Perform a request to the homeserver without any credentials.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} [body] The HTTP JSON body.
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {string=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
public request<T>(
method: Method,
path: string,
queryParams?: QueryDict,
body?: Body,
opts?: IRequestOpts,
): Promise<ResponseType<T, O>> {
const fullUri = this.getUrl(path, queryParams, opts?.prefix, opts?.baseUrl);
return this.requestOtherUrl<T>(method, fullUri, body, opts);
}
/**
* Perform a request to an arbitrary URL.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} url The HTTP URL object.
*
* @param {Object} [body] The HTTP JSON body.
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {Promise} Resolves to data unless `onlyData` is specified as false,
* where the resolved value will be a fetch Response object.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
public async requestOtherUrl<T>(
method: Method,
url: URL | string,
body?: Body,
opts: Pick<IRequestOpts, "headers" | "json" | "localTimeoutMs" | "abortSignal"> = {},
): Promise<ResponseType<T, O>> {
const headers = Object.assign({}, opts.headers || {});
const json = opts.json ?? true;
// We can't use getPrototypeOf here as objects made in other contexts e.g. over postMessage won't have same ref
const jsonBody = json && body?.constructor?.name === Object.name;
if (json) {
if (jsonBody && !headers["Content-Type"]) {
headers["Content-Type"] = "application/json";
}
if (!headers["Accept"]) {
headers["Accept"] = "application/json";
}
}
const timeout = opts.localTimeoutMs ?? this.opts.localTimeoutMs;
const signals = [
this.abortController.signal,
];
if (timeout !== undefined) {
signals.push(timeoutSignal(timeout));
}
if (opts.abortSignal) {
signals.push(opts.abortSignal);
}
let data: BodyInit;
if (jsonBody) {
data = JSON.stringify(body);
} else {
data = body as BodyInit;
}
const { signal, cleanup } = anySignal(signals);
let res: Response;
try {
res = await this.fetch(url, {
signal,
method,
body: data,
headers,
mode: "cors",
redirect: "follow",
referrer: "",
referrerPolicy: "no-referrer",
cache: "no-cache",
credentials: "omit", // we send credentials via headers
});
} catch (e) {
if (e.name === "AbortError") {
throw e;
}
throw new ConnectionError("fetch failed", e);
} finally {
cleanup();
}
if (!res.ok) {
throw parseErrorResponse(res, await res.text());
}
if (this.opts.onlyData) {
return json ? res.json() : res.text();
}
return res as ResponseType<T, O>;
}
/**
* Form and return a homeserver request URL based on the given path params and prefix.
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g. "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be urlencoded).
* @param {string} prefix The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix.
* @param {string} baseUrl The baseUrl to use e.g. "https://matrix.org/", defaulting to this.opts.baseUrl.
* @return {string} URL
*/
public getUrl(
path: string,
queryParams?: QueryDict,
prefix?: string,
baseUrl?: string,
): URL {
const url = new URL((baseUrl ?? this.opts.baseUrl) + (prefix ?? this.opts.prefix) + path);
if (queryParams) {
utils.encodeParams(queryParams, url.searchParams);
}
return url;
}
}

216
src/http-api/index.ts Normal file
View File

@ -0,0 +1,216 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FetchHttpApi } from "./fetch";
import { FileType, IContentUri, IHttpOpts, Upload, UploadOpts, UploadResponse } from "./interface";
import { MediaPrefix } from "./prefix";
import * as utils from "../utils";
import * as callbacks from "../realtime-callbacks";
import { Method } from "./method";
import { ConnectionError, MatrixError } from "./errors";
import { parseErrorResponse } from "./utils";
export * from "./interface";
export * from "./prefix";
export * from "./errors";
export * from "./method";
export * from "./utils";
export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
private uploads: Upload[] = [];
/**
* Upload content to the homeserver
*
* @param {object} file The object to upload. On a browser, something that
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a Buffer, String or ReadStream.
*
* @param {object} opts options object
*
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {boolean=} opts.includeFilename if false will not send the filename,
* e.g for encrypted file uploads where filename leaks are undesirable.
* Defaults to true.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>application/octet-stream</tt>.
*
* @param {boolean=} opts.rawResponse Return the raw body, rather than
* parsing the JSON. Defaults to false (except on node.js, where it
* defaults to true for backwards compatibility).
*
* @param {boolean=} opts.onlyContentUri Just return the content URI,
* rather than the whole body. Defaults to false (except on browsers,
* where it defaults to true for backwards compatibility). Ignored if
* opts.rawResponse is true.
*
* @param {Function=} opts.progressHandler Optional. Called when a chunk of
* data has been uploaded, with an object containing the fields `loaded`
* (number of bytes transferred) and `total` (total size, if known).
*
* @return {Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
*/
public uploadContent(file: FileType, opts: UploadOpts = {}): Promise<UploadResponse> {
const includeFilename = opts.includeFilename ?? true;
const abortController = opts.abortController ?? new AbortController();
// If the file doesn't have a mime type, use a default since the HS errors if we don't supply one.
const contentType = opts.type ?? (file as File).type ?? 'application/octet-stream';
const fileName = opts.name ?? (file as File).name;
const upload = {
loaded: 0,
total: 0,
abortController,
} as Upload;
const defer = utils.defer<UploadResponse>();
if (global.XMLHttpRequest) {
const xhr = new global.XMLHttpRequest();
const timeoutFn = function() {
xhr.abort();
defer.reject(new Error("Timeout"));
};
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
callbacks.clearTimeout(timeoutTimer);
try {
if (xhr.status === 0) {
throw new DOMException(xhr.statusText, "AbortError"); // mimic fetch API
}
if (!xhr.responseText) {
throw new Error('No response body.');
}
if (xhr.status >= 400) {
defer.reject(parseErrorResponse(xhr, xhr.responseText));
} else {
defer.resolve(JSON.parse(xhr.responseText));
}
} catch (err) {
if (err.name === "AbortError") {
defer.reject(err);
return;
}
(<MatrixError>err).httpStatus = xhr.status;
defer.reject(new ConnectionError("request failed", err));
}
break;
}
};
xhr.upload.onprogress = (ev: ProgressEvent) => {
callbacks.clearTimeout(timeoutTimer);
upload.loaded = ev.loaded;
upload.total = ev.total;
timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
opts.progressHandler?.({
loaded: ev.loaded,
total: ev.total,
});
};
const url = this.getUrl("/upload", undefined, MediaPrefix.R0);
if (includeFilename && fileName) {
url.searchParams.set("filename", encodeURIComponent(fileName));
}
if (!this.opts.useAuthorizationHeader && this.opts.accessToken) {
url.searchParams.set("access_token", encodeURIComponent(this.opts.accessToken));
}
xhr.open(Method.Post, url.href);
if (this.opts.useAuthorizationHeader && this.opts.accessToken) {
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
}
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(file);
abortController.signal.addEventListener("abort", () => {
xhr.abort();
});
} else {
const queryParams: utils.QueryDict = {};
if (includeFilename && fileName) {
queryParams.filename = fileName;
}
const headers: Record<string, string> = { "Content-Type": contentType };
this.authedRequest<UploadResponse>(
Method.Post, "/upload", queryParams, file, {
prefix: MediaPrefix.R0,
headers,
abortSignal: abortController.signal,
},
).then(response => {
return this.opts.onlyData ? <UploadResponse>response : response.json();
}).then(defer.resolve, defer.reject);
}
// remove the upload from the list on completion
upload.promise = defer.promise.finally(() => {
utils.removeElement(this.uploads, elem => elem === upload);
});
abortController.signal.addEventListener("abort", () => {
utils.removeElement(this.uploads, elem => elem === upload);
defer.reject(new DOMException("Aborted", "AbortError"));
});
this.uploads.push(upload);
return upload.promise;
}
public cancelUpload(promise: Promise<UploadResponse>): boolean {
const upload = this.uploads.find(u => u.promise === promise);
if (upload) {
upload.abortController.abort();
return true;
}
return false;
}
public getCurrentUploads(): Upload[] {
return this.uploads;
}
/**
* Get the content repository url with query parameters.
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
* path and query parameters respectively.
*/
public getContentUri(): IContentUri {
return {
base: this.opts.baseUrl,
path: MediaPrefix.R0 + "/upload",
params: {
access_token: this.opts.accessToken,
},
};
}
}

93
src/http-api/interface.ts Normal file
View File

@ -0,0 +1,93 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixError } from "./errors";
export interface IHttpOpts {
fetchFn?: typeof global.fetch;
baseUrl: string;
idBaseUrl?: string;
prefix: string;
extraParams?: Record<string, string>;
accessToken?: string;
useAuthorizationHeader?: boolean; // defaults to true
onlyData?: boolean;
localTimeoutMs?: number;
}
export interface IRequestOpts {
baseUrl?: string;
prefix?: string;
headers?: Record<string, string>;
abortSignal?: AbortSignal;
localTimeoutMs?: number;
json?: boolean; // defaults to true
// Set to true to prevent the request function from emitting a Session.logged_out event.
// This is intended for use on endpoints where M_UNKNOWN_TOKEN is a valid/notable error response,
// such as with token refreshes.
inhibitLogoutEmit?: boolean;
}
export interface IContentUri {
base: string;
path: string;
params: {
// eslint-disable-next-line camelcase
access_token: string;
};
}
export enum HttpApiEvent {
SessionLoggedOut = "Session.logged_out",
NoConsent = "no_consent",
}
export type HttpApiEventHandlerMap = {
[HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void;
[HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void;
};
export interface UploadProgress {
loaded: number;
total: number;
}
export interface UploadOpts {
name?: string;
type?: string;
includeFilename?: boolean;
progressHandler?(progress: UploadProgress): void;
abortController?: AbortController;
}
export interface Upload {
loaded: number;
total: number;
promise: Promise<UploadResponse>;
abortController: AbortController;
}
export interface UploadResponse {
// eslint-disable-next-line camelcase
content_uri: string;
}
export type FileType = XMLHttpRequestBodyInit;

22
src/http-api/method.ts Normal file
View File

@ -0,0 +1,22 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum Method {
Get = "GET",
Put = "PUT",
Post = "POST",
Delete = "DELETE",
}

53
src/http-api/prefix.ts Normal file
View File

@ -0,0 +1,53 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum ClientPrefix {
/**
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
*/
R0 = "/_matrix/client/r0",
/**
* A constant representing the URI path for the legacy release v1 of the Client-Server HTTP API.
*/
V1 = "/_matrix/client/v1",
/**
* A constant representing the URI path for Client-Server API endpoints versioned at v3.
*/
V3 = "/_matrix/client/v3",
/**
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
*/
Unstable = "/_matrix/client/unstable",
}
export enum IdentityPrefix {
/**
* URI path for v1 of the identity API
* @deprecated Use v2.
*/
V1 = "/_matrix/identity/api/v1",
/**
* URI path for the v2 identity API
*/
V2 = "/_matrix/identity/api/v2",
}
export enum MediaPrefix {
/**
* URI path for the media repo API
*/
R0 = "/_matrix/media/r0",
}

149
src/http-api/utils.ts Normal file
View File

@ -0,0 +1,149 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { parse as parseContentType, ParsedMediaType } from "content-type";
import { logger } from "../logger";
import { sleep } from "../utils";
import { ConnectionError, MatrixError } from "./errors";
// Ponyfill for https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
export function timeoutSignal(ms: number): AbortSignal {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, ms);
return controller.signal;
}
export function anySignal(signals: AbortSignal[]): {
signal: AbortSignal;
cleanup(): void;
} {
const controller = new AbortController();
function cleanup() {
for (const signal of signals) {
signal.removeEventListener("abort", onAbort);
}
}
function onAbort() {
controller.abort();
cleanup();
}
for (const signal of signals) {
if (signal.aborted) {
onAbort();
break;
}
signal.addEventListener("abort", onAbort);
}
return {
signal: controller.signal,
cleanup,
};
}
/**
* Attempt to turn an HTTP error response into a Javascript Error.
*
* If it is a JSON response, we will parse it into a MatrixError. Otherwise
* we return a generic Error.
*
* @param {XMLHttpRequest|Response} response response object
* @param {String} body raw body of the response
* @returns {Error}
*/
export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error {
let contentType: ParsedMediaType;
try {
contentType = getResponseContentType(response);
} catch (e) {
return e;
}
if (contentType?.type === "application/json" && body) {
return new MatrixError(JSON.parse(body), response.status);
}
if (contentType?.type === "text/plain") {
return new Error(`Server returned ${response.status} error: ${body}`);
}
return new Error(`Server returned ${response.status} error`);
}
function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest {
return "getResponseHeader" in response;
}
/**
* extract the Content-Type header from the response object, and
* parse it to a `{type, parameters}` object.
*
* returns null if no content-type header could be found.
*
* @param {XMLHttpRequest|Response} response response object
* @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
*/
function getResponseContentType(response: XMLHttpRequest | Response): ParsedMediaType | null {
let contentType: string | null;
if (isXhr(response)) {
contentType = response.getResponseHeader("Content-Type");
} else {
contentType = response.headers.get("Content-Type");
}
if (!contentType) return null;
try {
return parseContentType(contentType);
} catch (e) {
throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
}
}
/**
* Retries a network operation run in a callback.
* @param {number} maxAttempts maximum attempts to try
* @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again.
* @return {any} the result of the network operation
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
*/
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => Promise<T>): Promise<T> {
let attempts = 0;
let lastConnectionError: ConnectionError | null = null;
while (attempts < maxAttempts) {
try {
if (attempts > 0) {
const timeout = 1000 * Math.pow(2, attempts);
logger.log(`network operation failed ${attempts} times, retrying in ${timeout}ms...`);
await sleep(timeout);
}
return await callback();
} catch (err) {
if (err instanceof ConnectionError) {
attempts += 1;
lastConnectionError = err;
} else {
throw err;
}
}
}
throw lastConnectionError;
}

View File

@ -14,17 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as request from "request";
import * as matrixcs from "./matrix";
import * as utils from "./utils";
import { logger } from './logger';
if (matrixcs.getRequest()) {
if (global.__js_sdk_entrypoint) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
}
matrixcs.request(request);
global.__js_sdk_entrypoint = true;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires

View File

@ -55,41 +55,6 @@ export {
createNewMatrixCall,
} from "./webrtc/call";
// expose the underlying request object so different environments can use
// different request libs (e.g. request or browser-request)
let requestInstance;
/**
* The function used to perform HTTP requests. Only use this if you want to
* use a different HTTP library, e.g. Angular's <code>$http</code>. This should
* be set prior to calling {@link createClient}.
* @param {requestFunction} r The request function to use.
*/
export function request(r) {
requestInstance = r;
}
/**
* Return the currently-set request function.
* @return {requestFunction} The current request function.
*/
export function getRequest() {
return requestInstance;
}
/**
* Apply wrapping code around the request function. The wrapper function is
* installed as the new request handler, and when invoked it is passed the
* previous value, along with the options and callback arguments.
* @param {requestWrapperFunction} wrapper The wrapping function.
*/
export function wrapRequest(wrapper) {
const origRequest = requestInstance;
requestInstance = function(options, callback) {
return wrapper(origRequest, options, callback);
};
}
let cryptoStoreFactory = () => new MemoryCryptoStore;
/**
@ -128,15 +93,13 @@ export interface ICryptoCallbacks {
/**
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
* @param {(Object|string)} opts The configuration options for this client. If
* @param {(Object)} opts The configuration options for this client. If
* this is a string, it is assumed to be the base URL. These configuration
* options will be passed directly to {@link module:client.MatrixClient}.
* @param {Object} opts.store If not set, defaults to
* {@link module:store/memory.MemoryStore}.
* @param {Object} opts.scheduler If not set, defaults to
* {@link module:scheduler~MatrixScheduler}.
* @param {requestFunction} opts.request If not set, defaults to the function
* supplied to {@link request} which defaults to the request module from NPM.
*
* @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore
* crypto store implementation. Calls the factory supplied to
@ -148,13 +111,7 @@ export interface ICryptoCallbacks {
* @see {@link module:client.MatrixClient} for the full list of options for
* <code>opts</code>.
*/
export function createClient(opts: ICreateClientOpts | string) {
if (typeof opts === "string") {
opts = {
"baseUrl": opts,
};
}
opts.request = opts.request || requestInstance;
export function createClient(opts: ICreateClientOpts) {
opts.store = opts.store || new MemoryStore({
localStorage: global.localStorage,
});
@ -163,23 +120,6 @@ export function createClient(opts: ICreateClientOpts | string) {
return new MatrixClient(opts);
}
/**
* The request function interface for performing HTTP requests. This matches the
* API for the {@link https://github.com/request/request#requestoptions-callback|
* request NPM module}. The SDK will attempt to call this function in order to
* perform an HTTP request.
* @callback requestFunction
* @param {Object} opts The options for this HTTP request.
* @param {string} opts.uri The complete URI.
* @param {string} opts.method The HTTP method.
* @param {Object} opts.qs The query parameters to append to the URI.
* @param {Object} opts.body The JSON-serializable object.
* @param {boolean} opts.json True if this is a JSON request.
* @param {Object} opts._matrix_opts The underlying options set for
* {@link MatrixHttpApi}.
* @param {requestCallback} callback The request callback.
*/
/**
* A wrapper for the request function interface.
* @callback requestWrapperFunction

View File

@ -476,10 +476,8 @@ export class MSC3089TreeSpace {
info: Partial<IEncryptedFile>,
additionalContent?: IContent,
): Promise<ISendEventResponse> {
const mxc = await this.client.uploadContent(encryptedContents, {
const { content_uri: mxc } = await this.client.uploadContent(encryptedContents, {
includeFilename: false,
onlyContentUri: true,
rawResponse: false, // make this explicit otherwise behaviour is different on browser vs NodeJS
});
info.url = mxc;

View File

@ -24,7 +24,7 @@ import { logger } from './logger';
import { MatrixEvent } from "./models/event";
import { EventType } from "./@types/event";
import { IDeferred } from "./utils";
import { MatrixError } from "./http-api";
import { ConnectionError, MatrixError } from "./http-api";
import { ISendEventResponse } from "./@types/requests";
const DEBUG = false; // set true to enable console logging.
@ -68,9 +68,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
// client error; no amount of retrying with save you now.
return -1;
}
// we ship with browser-request which returns { cors: rejected } when trying
// with no connection, so if we match that, give up since they have no conn.
if (err["cors"] === "rejected") {
if (err instanceof ConnectionError) {
return -1;
}

View File

@ -674,7 +674,7 @@ export class SlidingSyncSdk {
member._requestedProfileInfo = true;
// try to get a cached copy first.
const user = client.getUser(member.userId);
let promise;
let promise: ReturnType<MatrixClient["getProfileInfo"]>;
if (user) {
promise = Promise.resolve({
avatar_url: user.avatarUrl,

View File

@ -15,11 +15,11 @@ limitations under the License.
*/
import { logger } from './logger';
import { IAbortablePromise } from "./@types/partials";
import { MatrixClient } from "./client";
import { IRoomEvent, IStateEvent } from "./sync-accumulator";
import { TypedEventEmitter } from "./models//typed-event-emitter";
import { TypedEventEmitter } from "./models/typed-event-emitter";
import { sleep, IDeferred, defer } from "./utils";
import { ConnectionError } from "./http-api";
// /sync requests allow you to set a timeout= but the request may continue
// beyond that and wedge forever, so we need to track how long we are willing
@ -353,7 +353,8 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
private desiredRoomSubscriptions = new Set<string>(); // the *desired* room subscriptions
private confirmedRoomSubscriptions = new Set<string>();
private pendingReq?: IAbortablePromise<MSC3575SlidingSyncResponse>;
private pendingReq?: Promise<MSC3575SlidingSyncResponse>;
private abortController?: AbortController;
/**
* Create a new sliding sync instance
@ -700,7 +701,8 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
...d,
txnId: this.txnId,
});
this.pendingReq?.abort();
this.abortController?.abort();
this.abortController = new AbortController();
return d.promise;
}
@ -728,7 +730,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
this.txnIdDefers[i].reject(this.txnIdDefers[i].txnId);
}
this.txnIdDefers[txnIndex].resolve(txnId);
// clear out settled promises, incuding the one we resolved.
// clear out settled promises, including the one we resolved.
this.txnIdDefers = this.txnIdDefers.slice(txnIndex+1);
}
@ -737,7 +739,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
*/
public stop(): void {
this.terminated = true;
this.pendingReq?.abort();
this.abortController?.abort();
// remove all listeners so things can be GC'd
this.removeAllListeners(SlidingSyncEvent.Lifecycle);
this.removeAllListeners(SlidingSyncEvent.List);
@ -748,6 +750,8 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* Start syncing with the server. Blocks until stopped.
*/
public async start() {
this.abortController = new AbortController();
let currentPos: string;
while (!this.terminated) {
this.needsResend = false;
@ -780,7 +784,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
reqBody.txn_id = this.txnId;
this.txnId = null;
}
this.pendingReq = this.client.slidingSync(reqBody, this.proxyBaseUrl);
this.pendingReq = this.client.slidingSync(reqBody, this.proxyBaseUrl, this.abortController.signal);
resp = await this.pendingReq;
logger.debug(resp);
currentPos = resp.pos;
@ -821,10 +825,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
err,
);
await sleep(3000);
} else if (this.needsResend || err === "aborted") {
// don't sleep as we caused this error by abort()ing the request.
// we check for 'aborted' because that's the error Jest returns and without it
// we get warnings about not exiting fast enough.
} else if (this.needsResend || err instanceof ConnectionError) {
continue;
} else {
logger.error(err);

View File

@ -247,7 +247,7 @@ export interface IStore {
/**
* Fetches the oldest batch of to-device messages in the queue
*/
getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch>;
getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null>;
/**
* Removes a specific batch of to-device messages from the queue

View File

@ -33,7 +33,7 @@ export interface IIndexedDBBackend {
getClientOptions(): Promise<IStartClientOpts>;
storeClientOptions(options: IStartClientOpts): Promise<void>;
saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void>;
getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch>;
getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null>;
removeToDeviceBatch(id: number): Promise<void>;
}

View File

@ -138,7 +138,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
return this.doCmd('saveToDeviceBatches', [batches]);
}
public async getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch> {
public async getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> {
return this.doCmd('getOldestToDeviceBatch');
}

View File

@ -357,7 +357,7 @@ export class IndexedDBStore extends MemoryStore {
return this.backend.saveToDeviceBatches(batches);
}
public getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch> {
public getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> {
return this.backend.getOldestToDeviceBatch();
}

View File

@ -57,7 +57,6 @@ import { RoomStateEvent, IMarkerFoundOptions } from "./models/room-state";
import { RoomMemberEvent } from "./models/room-member";
import { BeaconEvent } from "./models/beacon";
import { IEventsResponse } from "./@types/requests";
import { IAbortablePromise } from "./@types/partials";
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
import { Feature, ServerSupport } from "./feature";
@ -164,7 +163,8 @@ type WrappedRoom<T> = T & {
*/
export class SyncApi {
private _peekRoom: Optional<Room> = null;
private currentSyncRequest: Optional<IAbortablePromise<ISyncResponse>> = null;
private currentSyncRequest: Optional<Promise<ISyncResponse>> = null;
private abortController?: AbortController;
private syncState: Optional<SyncState> = null;
private syncStateData: Optional<ISyncStateData> = null; // additional data (eg. error object for failed sync)
private catchingUp = false;
@ -298,9 +298,9 @@ export class SyncApi {
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter,
).then(function(filterId) {
qps.filter = filterId;
return client.http.authedRequest<ISyncResponse>(
undefined, Method.Get, "/sync", qps as any, undefined, localTimeoutMs,
);
return client.http.authedRequest<ISyncResponse>(Method.Get, "/sync", qps as any, undefined, {
localTimeoutMs,
});
}).then(async (data) => {
let leaveRooms = [];
if (data.rooms?.leave) {
@ -433,11 +433,11 @@ export class SyncApi {
}
// FIXME: gut wrenching; hard-coded timeout values
this.client.http.authedRequest<IEventsResponse>(undefined, Method.Get, "/events", {
this.client.http.authedRequest<IEventsResponse>(Method.Get, "/events", {
room_id: peekRoom.roomId,
timeout: String(30 * 1000),
from: token,
}, undefined, 50 * 1000).then((res) => {
}, undefined, { localTimeoutMs: 50 * 1000 }).then((res) => {
if (this._peekRoom !== peekRoom) {
debuglog("Stopped peeking in room %s", peekRoom.roomId);
return;
@ -652,6 +652,7 @@ export class SyncApi {
*/
public async sync(): Promise<void> {
this.running = true;
this.abortController = new AbortController();
global.window?.addEventListener?.("online", this.onOnline, false);
@ -738,7 +739,7 @@ export class SyncApi {
// but do not have global.window.removeEventListener.
global.window?.removeEventListener?.("online", this.onOnline, false);
this.running = false;
this.currentSyncRequest?.abort();
this.abortController?.abort();
if (this.keepAliveTimer) {
clearTimeout(this.keepAliveTimer);
this.keepAliveTimer = null;
@ -902,12 +903,12 @@ export class SyncApi {
}
}
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IAbortablePromise<ISyncResponse> {
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): Promise<ISyncResponse> {
const qps = this.getSyncParams(syncOptions, syncToken);
return this.client.http.authedRequest<ISyncResponse>(
undefined, Method.Get, "/sync", qps as any, undefined,
qps.timeout + BUFFER_PERIOD_MS,
);
return this.client.http.authedRequest<ISyncResponse>(Method.Get, "/sync", qps as any, undefined, {
localTimeoutMs: qps.timeout + BUFFER_PERIOD_MS,
abortSignal: this.abortController?.signal,
});
}
private getSyncParams(syncOptions: ISyncOptions, syncToken: string): ISyncParams {
@ -1521,7 +1522,6 @@ export class SyncApi {
};
this.client.http.request(
undefined, // callback
Method.Get, "/_matrix/client/versions",
undefined, // queryParams
undefined, // data

View File

@ -59,17 +59,23 @@ export function internaliseString(str: string): string {
* {"foo": "bar", "baz": "taz"}
* @return {string} The encoded string e.g. foo=bar&baz=taz
*/
export function encodeParams(params: Record<string, string | number | boolean>): string {
const searchParams = new URLSearchParams();
export function encodeParams(params: QueryDict, urlSearchParams?: URLSearchParams): URLSearchParams {
const searchParams = urlSearchParams ?? new URLSearchParams();
for (const [key, val] of Object.entries(params)) {
if (val !== undefined && val !== null) {
searchParams.set(key, String(val));
if (Array.isArray(val)) {
val.forEach(v => {
searchParams.append(key, String(v));
});
} else {
searchParams.append(key, String(val));
}
}
return searchParams.toString();
}
return searchParams;
}
export type QueryDict = Record<string, string | string[]>;
export type QueryDict = Record<string, string[] | string | number | boolean | undefined>;
/**
* Decode a query string in `application/x-www-form-urlencoded` format.
@ -80,8 +86,8 @@ export type QueryDict = Record<string, string | string[]>;
* This behaviour matches Node's qs.parse but is built on URLSearchParams
* for native web compatibility
*/
export function decodeParams(query: string): QueryDict {
const o: QueryDict = {};
export function decodeParams(query: string): Record<string, string | string[]> {
const o: Record<string, string | string[]> = {};
const params = new URLSearchParams(query);
for (const key of params.keys()) {
const val = params.getAll(key);

View File

@ -298,7 +298,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// yet, null if we have but they didn't send a party ID.
private opponentPartyId: string;
private opponentCaps: CallCapabilities;
private inviteTimeout: ReturnType<typeof setTimeout>;
private inviteTimeout?: ReturnType<typeof setTimeout>;
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
// This flag represents whether we want the other party to be on hold
@ -322,7 +322,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private remoteSDPStreamMetadata: SDPStreamMetadata;
private callLengthInterval: ReturnType<typeof setInterval>;
private callLengthInterval?: ReturnType<typeof setInterval>;
private callLength = 0;
constructor(opts: CallOpts) {
@ -1689,7 +1689,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.inviteOrAnswerSent = true;
this.setState(CallState.InviteSent);
this.inviteTimeout = setTimeout(() => {
this.inviteTimeout = null;
this.inviteTimeout = undefined;
if (this.state === CallState.InviteSent) {
this.hangup(CallErrorCode.InviteTimeout, false);
}
@ -2004,11 +2004,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
if (this.inviteTimeout) {
clearTimeout(this.inviteTimeout);
this.inviteTimeout = null;
this.inviteTimeout = undefined;
}
if (this.callLengthInterval) {
clearInterval(this.callLengthInterval);
this.callLengthInterval = null;
this.callLengthInterval = undefined;
}
// Order is important here: first we stopAllMedia() and only then we can deleteAllFeeds()

339
yarn.lock
View File

@ -1588,16 +1588,18 @@
dependencies:
base-x "^3.0.6"
"@types/caseless@*":
version "0.12.2"
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
"@types/content-type@^1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.5.tgz#aa02dca40864749a9e2bf0161a6216da57e3ede5"
integrity sha512-dgMN+syt1xb7Hk8LU6AODOfPlvz5z1CbXpPuJE5ZrX9STfBOIXF09pEB8N7a97WT9dbngt3ksDCm6GW6yMrxfQ==
"@types/domexception@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/domexception/-/domexception-4.0.0.tgz#bb19c920c81c3f1408b46d021fb79467ff2d32fd"
integrity sha512-vD9eLiVhgDrsVtUIiHBaToW/lfhCUYzmb81Sc0a0kJ4qEN2ZJyhz4wVyAmum4hAVDuBMUDE4yAeeF7fPHKCujw==
dependencies:
"@types/webidl-conversions" "*"
"@types/graceful-fs@^4.1.3":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
@ -1675,16 +1677,6 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e"
integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==
"@types/request@^2.48.5":
version "2.48.8"
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c"
integrity sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==
dependencies:
"@types/caseless" "*"
"@types/node" "*"
"@types/tough-cookie" "*"
form-data "^2.5.0"
"@types/retry@0.12.0":
version "0.12.0"
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
@ -1695,10 +1687,10 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/tough-cookie@*":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
"@types/webidl-conversions@*":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz#2b8e60e33906459219aa587e9d1a612ae994cfe7"
integrity sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==
"@types/yargs-parser@*":
version "21.0.0"
@ -1858,7 +1850,7 @@ acorn@^8.5.0, acorn@^8.8.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4:
ajv@^6.10.0, ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@ -1993,18 +1985,6 @@ asn1.js@^5.2.0:
minimalistic-assert "^1.0.0"
safer-buffer "^2.1.0"
asn1@~0.2.3:
version "0.2.6"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
dependencies:
safer-buffer "~2.1.0"
assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
assert@^1.4.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb"
@ -2025,26 +2005,11 @@ ast-types@^0.14.2:
dependencies:
tslib "^2.0.1"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
available-typed-arrays@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==
aws4@^1.8.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
babel-jest@^29.0.0, babel-jest@^29.1.2:
version "29.1.2"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.1.2.tgz#540d3241925c55240fb0c742e3ffc5f33a501978"
@ -2191,13 +2156,6 @@ base64-js@^1.0.2:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
bcrypt-pbkdf@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
dependencies:
tweetnacl "^0.14.3"
before-after-hook@^2.2.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e"
@ -2274,11 +2232,6 @@ browser-pack@^6.0.1:
through2 "^2.0.0"
umd "^3.0.0"
browser-request@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/browser-request/-/browser-request-0.3.3.tgz#9ece5b5aca89a29932242e18bf933def9876cc17"
integrity sha512-YyNI4qJJ+piQG6MMEuo7J3Bzaqssufx04zpEKYfSrl/1Op59HWali9zMtBpXnkmqMcOuWJPZvudrm9wISmnCbg==
browser-resolve@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-2.0.0.tgz#99b7304cb392f8d73dba741bb2d7da28c6d7842b"
@ -2504,11 +2457,6 @@ caniuse-lite@^1.0.30001400:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001414.tgz#5f1715e506e71860b4b07c50060ea6462217611e"
integrity sha512-t55jfSaWjCdocnFdKQoO+d2ct9C59UZg4dY3OnUlSZ447r8pUtIKdp0hpAzrGFultmTC+Us+KpKi4GZl/LXlFg==
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
catharsis@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121"
@ -2684,13 +2632,6 @@ combine-source-map@^0.8.0, combine-source-map@~0.8.0:
lodash.memoize "~3.0.3"
source-map "~0.5.3"
combined-stream@^1.0.6, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
commander@^2.19.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@ -2770,11 +2711,6 @@ core-js@^2.4.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-util-is@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
@ -2850,13 +2786,6 @@ dash-ast@^1.0.0:
resolved "https://registry.yarnpkg.com/dash-ast/-/dash-ast-1.0.0.tgz#12029ba5fb2f8aa6f0a861795b23c1b4b6c27d37"
integrity sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==
dependencies:
assert-plus "^1.0.0"
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
@ -2921,11 +2850,6 @@ defined@^1.0.0:
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
integrity sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
deprecation@^2.0.0, deprecation@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
@ -3030,6 +2954,13 @@ domexception@^1.0.1:
dependencies:
webidl-conversions "^4.0.2"
domexception@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673"
integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==
dependencies:
webidl-conversions "^7.0.0"
duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@ -3037,14 +2968,6 @@ duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
dependencies:
readable-stream "^2.0.2"
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==
dependencies:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
electron-to-chromium@^1.4.251:
version "1.4.270"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.270.tgz#2c6ea409b45cdb5c3e0cb2c08cf6c0ba7e0f2c26"
@ -3467,21 +3390,6 @@ ext@^1.1.2:
dependencies:
type "^2.7.2"
extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==
extsprintf@^1.2.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
fake-indexeddb@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-4.0.0.tgz#1dfb2023a3be175e35a6d84975218b432041934d"
@ -3608,29 +3516,6 @@ foreground-child@^2.0.0:
cross-spawn "^7.0.0"
signal-exit "^3.0.2"
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==
form-data@^2.5.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.6"
mime-types "^2.1.12"
form-data@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.6"
mime-types "^2.1.12"
fs-readdir-recursive@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27"
@ -3713,13 +3598,6 @@ get-tsconfig@^4.2.0:
resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.2.0.tgz#ff368dd7104dab47bf923404eb93838245c66543"
integrity sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==
dependencies:
assert-plus "^1.0.0"
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@ -3801,19 +3679,6 @@ grapheme-splitter@^1.0.4:
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==
har-validator@~5.1.3:
version "5.1.5"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
dependencies:
ajv "^6.12.3"
har-schema "^2.0.0"
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
@ -3901,15 +3766,6 @@ htmlescape@^1.1.0:
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
integrity sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==
dependencies:
assert-plus "^1.0.0"
jsprim "^1.2.2"
sshpk "^1.7.0"
https-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
@ -4181,11 +4037,6 @@ is-typed-array@^1.1.3, is-typed-array@^1.1.9:
for-each "^0.3.3"
has-tostringtag "^1.0.0"
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
is-utf8@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
@ -4220,11 +4071,6 @@ isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==
istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
@ -4737,11 +4583,6 @@ js2xmlparser@^4.0.2:
dependencies:
xmlcreate "^2.0.4"
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==
jsdoc@^3.6.6:
version "3.6.11"
resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.6.11.tgz#8bbb5747e6f579f141a5238cbad4e95e004458ce"
@ -4783,21 +4624,11 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-schema@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
json5@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
@ -4815,16 +4646,6 @@ jsonparse@^1.2.0:
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
jsprim@^1.2.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==
dependencies:
assert-plus "1.0.0"
extsprintf "1.3.0"
json-schema "0.4.0"
verror "1.10.0"
jstransformer@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
@ -5034,10 +4855,10 @@ matrix-events-sdk@^0.0.1-beta.7:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934"
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
matrix-mock-request@^2.1.2:
version "2.4.1"
resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.4.1.tgz#a9c7dbb6b466f582ba2ca21f17cf18ceb41c7657"
integrity sha512-QMNpKUeHS2RHovSKybUySFTXTJ11EQPkp3bgvEXmNqAc3TYM23gKYqgI288BoBDYwQrK3WJFT0d4bvMiNIS/vA==
matrix-mock-request@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.5.0.tgz#78da2590e82be2e31edcf9814833af5e5f8d2f1a"
integrity sha512-7T3gklpW+4rfHsTnp/FDML7aWoBrXhAh8+1ltinQfAh9TDj6y382z/RUMR7i03d1WDzt/ed1UTihqO5GDoOq9Q==
dependencies:
expect "^28.1.0"
@ -5095,18 +4916,6 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12, mime-types@~2.1.19:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@ -5244,11 +5053,6 @@ npm-run-path@^4.0.1:
dependencies:
path-key "^3.0.0"
oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -5469,11 +5273,6 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@ -5576,11 +5375,6 @@ pseudomap@^1.0.2:
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==
psl@^1.1.28:
version "1.9.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
public-encrypt@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@ -5720,11 +5514,6 @@ qs@^6.9.6:
dependencies:
side-channel "^1.0.4"
qs@~6.5.2:
version "6.5.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
querystring-es3@~0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@ -5924,32 +5713,6 @@ repeat-string@^1.5.2:
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
request@^2.88.2:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
caseless "~0.12.0"
combined-stream "~1.0.6"
extend "~3.0.2"
forever-agent "~0.6.1"
form-data "~2.3.2"
har-validator "~5.1.3"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.19"
oauth-sign "~0.9.0"
performance-now "^2.1.0"
qs "~6.5.2"
safe-buffer "^5.1.2"
tough-cookie "~2.5.0"
tunnel-agent "^0.6.0"
uuid "^3.3.2"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@ -6051,7 +5814,7 @@ safe-regex-test@^1.0.0:
get-intrinsic "^1.1.3"
is-regex "^1.1.4"
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
safer-buffer@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@ -6189,21 +5952,6 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
sshpk@^1.7.0:
version "1.17.0"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
bcrypt-pbkdf "^1.0.0"
dashdash "^1.12.0"
ecc-jsbn "~0.1.1"
getpass "^0.1.1"
jsbn "~0.1.0"
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
stack-utils@^2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5"
@ -6506,14 +6254,6 @@ token-stream@0.0.1:
resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-0.0.1.tgz#ceeefc717a76c4316f126d0b9dbaa55d7e7df01a"
integrity sha512-nfjOAu/zAWmX9tgwi5NRp7O7zTDUD1miHiB40klUnAh9qnL1iXdgzcz/i5dMaL5jahcBAaSfmNOBBJBLJW8TEg==
tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
dependencies:
psl "^1.1.28"
punycode "^2.1.1"
tr46@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240"
@ -6585,23 +6325,11 @@ tty-browserify@0.0.1:
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
dependencies:
safe-buffer "^5.0.1"
tunnel@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@ -6794,11 +6522,6 @@ util@~0.12.0:
safe-buffer "^5.1.2"
which-typed-array "^1.1.2"
uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
@ -6813,15 +6536,6 @@ v8-to-istanbul@^9.0.0, v8-to-istanbul@^9.0.1:
"@types/istanbul-lib-coverage" "^2.0.1"
convert-source-map "^1.6.0"
verror@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==
dependencies:
assert-plus "^1.0.0"
core-util-is "1.0.2"
extsprintf "^1.2.0"
vm-browserify@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
@ -6885,6 +6599,11 @@ webidl-conversions@^6.1.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
webidl-conversions@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"