diff --git a/README.md b/README.md index ee41f49b0e2..add491156dd 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a browser or in Node.js. +#### Minimum Matrix server version: v1.1 + The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only guarantees that a feature will be supported for at least 4 spec releases. For example, if a feature the js-sdk supports is removed in v1.4 then the feature is _eligible_ for removal from the SDK when v1.8 is released. This SDK has no diff --git a/spec/TestClient.ts b/spec/TestClient.ts index 0a3d25425e3..7d23cadfffa 100644 --- a/spec/TestClient.ts +++ b/spec/TestClient.ts @@ -90,7 +90,7 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder { logger.log(this + ": starting"); this.httpBackend.when("GET", "/versions").respond(200, { // we have tests that rely on support for lazy-loading members - versions: ["r0.5.0"], + versions: ["v1.1"], }); this.httpBackend.when("GET", "/pushrules").respond(200, {}); this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 8d2b386c413..fd578084fb6 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -1945,7 +1945,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, }; }; - for (const path of ["/_matrix/client/r0/keys/upload", "/_matrix/client/v3/keys/upload"]) { + for (const path of ["/_matrix/client/v3/keys/upload", "/_matrix/client/v3/keys/upload"]) { fetchMock.post(new URL(path, aliceClient.getHomeserverUrl()).toString(), listener, { // These routes are already defined in the E2EKeyReceiver // We want to overwrite the behaviour of the E2EKeyReceiver @@ -2082,9 +2082,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, return queryResponseBody; }; - for (const path of ["/_matrix/client/r0/keys/query", "/_matrix/client/v3/keys/query"]) { - fetchMock.post(new URL(path, aliceClient.getHomeserverUrl()).toString(), listener); - } + fetchMock.post( + new URL("/_matrix/client/v3/keys/query", aliceClient.getHomeserverUrl()).toString(), + listener, + ); }); } @@ -2175,7 +2176,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, }); /** - * Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/:type(m.secret_storage.*)` + * Create a mock to respond to the PUT request `/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)` * Resolved when a key is uploaded (ie in `body.content.key`) * https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype */ @@ -2184,7 +2185,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, // This url is called multiple times during the secret storage bootstrap process // When we received the newly generated key, we return it fetchMock.put( - "express:/_matrix/client/r0/user/:userId/account_data/:type(m.secret_storage.*)", + "express:/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)", (url: string, options: RequestInit) => { const content = JSON.parse(options.body as string); @@ -2200,7 +2201,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, } /** - * Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/m.cross_signing.${key}` + * Create a mock to respond to the PUT request `/_matrix/client/v3/user/:userId/account_data/m.cross_signing.${key}` * Resolved when the cross signing key is uploaded * https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype */ @@ -2208,7 +2209,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, return new Promise((resolve) => { // Called when the cross signing key is uploaded fetchMock.put( - `express:/_matrix/client/r0/user/:userId/account_data/m.cross_signing.${key}`, + `express:/_matrix/client/v3/user/:userId/account_data/m.cross_signing.${key}`, (url: string, options: RequestInit) => { const content = JSON.parse(options.body as string); resolve(content.encrypted); diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 26e968d7154..0d519cbc596 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -1342,7 +1342,7 @@ describe("MatrixClient event timelines", function () { function respondToContext(event: Partial = THREAD_ROOT): ExpectedHttpRequest { const request = httpBackend.when( "GET", - encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", { + encodeUri("/_matrix/client/v3/rooms/$roomId/context/$eventId", { $roomId: roomId, $eventId: event.event_id!, }), @@ -1360,7 +1360,7 @@ describe("MatrixClient event timelines", function () { function respondToEvent(event: Partial = THREAD_ROOT): ExpectedHttpRequest { const request = httpBackend.when( "GET", - encodeUri("/_matrix/client/r0/rooms/$roomId/event/$eventId", { + encodeUri("/_matrix/client/v3/rooms/$roomId/event/$eventId", { $roomId: roomId, $eventId: event.event_id!, }), @@ -1371,7 +1371,7 @@ describe("MatrixClient event timelines", function () { function respondToMessagesRequest(): ExpectedHttpRequest { const request = httpBackend.when( "GET", - encodeUri("/_matrix/client/r0/rooms/$roomId/messages", { + encodeUri("/_matrix/client/v3/rooms/$roomId/messages", { $roomId: roomId, }), ); diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index 89f69069a6a..977190abddb 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -73,7 +73,7 @@ describe("MatrixClient", function () { it("should upload the file", function () { httpBackend - .when("POST", "/_matrix/media/r0/upload") + .when("POST", "/_matrix/media/v3/upload") .check(function (req) { expect(req.rawData).toEqual(buf); expect(req.queryParams?.filename).toEqual("hi.txt"); @@ -108,7 +108,7 @@ describe("MatrixClient", function () { it("should parse errors into a MatrixError", function () { httpBackend - .when("POST", "/_matrix/media/r0/upload") + .when("POST", "/_matrix/media/v3/upload") .check(function (req) { expect(req.rawData).toEqual(buf); // @ts-ignore private property @@ -708,7 +708,7 @@ describe("MatrixClient", function () { const auth = { identifier: 1 }; it("should pass through an auth dict", function () { httpBackend - .when("DELETE", "/_matrix/client/r0/devices/my_device") + .when("DELETE", "/_matrix/client/v3/devices/my_device") .check(function (req) { expect(req.data).toEqual({ auth: auth }); }) @@ -1102,10 +1102,6 @@ describe("MatrixClient", function () { submit_url: "https://foobar.matrix/_matrix/matrix", }; - httpBackend.when("GET", "/_matrix/client/versions").respond(200, { - versions: ["r0.6.0"], - }); - const prom = client.requestRegisterEmailToken("bob@email", "secret", 1); httpBackend .when("POST", "/register/email/requestToken") @@ -1126,10 +1122,6 @@ describe("MatrixClient", function () { it("should supply an id_access_token", async () => { const targetEmail = "gerald@example.org"; - httpBackend.when("GET", "/_matrix/client/versions").respond(200, { - versions: ["r0.6.0"], - }); - httpBackend .when("POST", "/invite") .check((req) => { @@ -1165,10 +1157,6 @@ describe("MatrixClient", function () { ], }; - httpBackend.when("GET", "/_matrix/client/versions").respond(200, { - versions: ["r0.6.0"], - }); - httpBackend .when("POST", "/createRoom") .check((req) => { @@ -1652,6 +1640,82 @@ describe("MatrixClient", function () { ]); }); }); + + describe("getFallbackAuthUrl", () => { + it("should return fallback url", () => { + expect(client.getFallbackAuthUrl("loginType", "authSessionId")).toMatchInlineSnapshot( + `"http://alice.localhost.test.server/_matrix/client/v3/auth/loginType/fallback/web?session=authSessionId"`, + ); + }); + }); + + describe("addThreePidOnly", () => { + it("should make expected POST request", async () => { + httpBackend + .when("POST", "/_matrix/client/v3/account/3pid/add") + .check(function (req) { + expect(req.data).toEqual({ + client_secret: "secret", + sid: "sid", + }); + expect(req.headers["Authorization"]).toBe("Bearer " + accessToken); + }) + .respond(200, {}); + + await Promise.all([ + client.addThreePidOnly({ + client_secret: "secret", + sid: "sid", + }), + httpBackend.flushAllExpected(), + ]); + }); + }); + + describe("bindThreePid", () => { + it("should make expected POST request", async () => { + httpBackend + .when("POST", "/_matrix/client/v3/account/3pid/bind") + .check(function (req) { + expect(req.data).toEqual({ + client_secret: "secret", + id_server: "server", + id_access_token: "token", + sid: "sid", + }); + expect(req.headers["Authorization"]).toBe("Bearer " + accessToken); + }) + .respond(200, {}); + + await Promise.all([ + client.bindThreePid({ + client_secret: "secret", + id_server: "server", + id_access_token: "token", + sid: "sid", + }), + httpBackend.flushAllExpected(), + ]); + }); + }); + + describe("unbindThreePid", () => { + it("should make expected POST request", async () => { + httpBackend + .when("POST", "/_matrix/client/v3/account/3pid/unbind") + .check(function (req) { + expect(req.data).toEqual({ + medium: "email", + address: "alice@server.com", + id_server: "identity.localhost", + }); + expect(req.headers["Authorization"]).toBe("Bearer " + accessToken); + }) + .respond(200, {}); + + await Promise.all([client.unbindThreePid("email", "alice@server.com"), httpBackend.flushAllExpected()]); + }); + }); }); function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent { diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index 45b6f42b4a6..cea06d10d87 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -224,8 +224,6 @@ describe("MatrixClient syncing", () => { }); it("should honour lazyLoadMembers if user is not a guest", () => { - client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true); - httpBackend! .when("GET", "/sync") .check((req) => { @@ -242,8 +240,6 @@ describe("MatrixClient syncing", () => { it("should not honour lazyLoadMembers if user is a guest", () => { httpBackend!.expectedRequests = []; httpBackend!.when("GET", "/versions").respond(200, {}); - client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true); - httpBackend! .when("GET", "/sync") .check((req) => { diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index a55316b5fa5..4785ca73b9c 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -121,7 +121,7 @@ describe("SlidingSyncSdk", () => { await client!.initCrypto(); syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto; } - httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {}); + httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {}); sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts); }; diff --git a/spec/test-utils/E2EKeyReceiver.ts b/spec/test-utils/E2EKeyReceiver.ts index 44a863e8b07..24af06d6e37 100644 --- a/spec/test-utils/E2EKeyReceiver.ts +++ b/spec/test-utils/E2EKeyReceiver.ts @@ -75,8 +75,6 @@ export class E2EKeyReceiver implements IE2EKeyReceiver { const listener = (url: string, options: RequestInit) => this.onKeyUploadRequest(resolveOneTimeKeys, options); - // catch both r0 and v3 variants - fetchMock.post(new URL("/_matrix/client/r0/keys/upload", homeserverUrl).toString(), listener); fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener); }); } diff --git a/spec/test-utils/E2EKeyResponder.ts b/spec/test-utils/E2EKeyResponder.ts index e779e8c5e87..57152f17621 100644 --- a/spec/test-utils/E2EKeyResponder.ts +++ b/spec/test-utils/E2EKeyResponder.ts @@ -43,8 +43,6 @@ export class E2EKeyResponder { public constructor(homeserverUrl: string) { // set up a listener for /keys/query. const listener = (url: string, options: RequestInit) => this.onKeyQueryRequest(options); - // catch both r0 and v3 variants - fetchMock.post(new URL("/_matrix/client/r0/keys/query", homeserverUrl).toString(), listener); fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener); } diff --git a/spec/test-utils/SyncResponder.ts b/spec/test-utils/SyncResponder.ts index 56a9c30c8ac..31fae7147a8 100644 --- a/spec/test-utils/SyncResponder.ts +++ b/spec/test-utils/SyncResponder.ts @@ -75,7 +75,7 @@ export class SyncResponder implements ISyncResponder { */ public constructor(homeserverUrl: string) { this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`); - fetchMock.get("begin:" + new URL("/_matrix/client/r0/sync?", homeserverUrl).toString(), (_url, _options) => + fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (_url, _options) => this.onSyncRequest(), ); } diff --git a/spec/test-utils/client.ts b/spec/test-utils/client.ts index 1e8a4643893..8123d0d65e6 100644 --- a/spec/test-utils/client.ts +++ b/spec/test-utils/client.ts @@ -86,7 +86,6 @@ export const mockClientMethodsEvents = () => ({ * Returns basic mocked client methods related to server support */ export const mockClientMethodsServer = (): Partial, unknown>> => ({ - doesServerSupportSeparateAddAndBind: jest.fn(), getIdentityServerUrl: jest.fn(), getHomeserverUrl: jest.fn(), getCapabilities: jest.fn().mockReturnValue({}), diff --git a/spec/test-utils/mockEndpoints.ts b/spec/test-utils/mockEndpoints.ts index 386b7a4cb00..4fdcc5b1a81 100644 --- a/spec/test-utils/mockEndpoints.ts +++ b/spec/test-utils/mockEndpoints.ts @@ -22,9 +22,9 @@ import fetchMock from "fetch-mock-jest"; * @param homeserverUrl - the homeserver url for the client under test */ export function mockInitialApiRequests(homeserverUrl: string) { - fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["r0.5.0"] }); - fetchMock.getOnce(new URL("/_matrix/client/r0/pushrules/", homeserverUrl).toString(), {}); - fetchMock.postOnce(new URL("/_matrix/client/r0/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), { + fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] }); + fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {}); + fetchMock.postOnce(new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), { filter_id: "fid", }); } @@ -32,13 +32,13 @@ export function mockInitialApiRequests(homeserverUrl: string) { /** * Mock the requests needed to set up cross signing * - * Return 404 error for `GET _matrix/client/r0/user/:userId/account_data/:type` request + * Return 404 error for `GET _matrix/client/v3/user/:userId/account_data/:type` request * Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check) * Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check) */ export function mockSetupCrossSigningRequests(): void { // have account_data requests return an empty object - fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", { + fetchMock.get("express:/_matrix/client/v3/user/:userId/account_data/:type", { status: 404, body: { errcode: "M_NOT_FOUND", error: "Account data not found." }, }); diff --git a/spec/unit/autodiscovery.spec.ts b/spec/unit/autodiscovery.spec.ts index 2ffe9be1a68..b7614d7240f 100644 --- a/spec/unit/autodiscovery.spec.ts +++ b/spec/unit/autodiscovery.spec.ts @@ -18,7 +18,7 @@ limitations under the License. import fetchMock from "fetch-mock-jest"; import MockHttpBackend from "matrix-mock-request"; -import { M_AUTHENTICATION } from "../../src"; +import { AutoDiscoveryAction, M_AUTHENTICATION } from "../../src"; import { AutoDiscovery } from "../../src/autodiscovery"; import { OidcError } from "../../src/oidc/error"; import { makeDelegatedAuthConfig } from "../test-utils/oidc"; @@ -351,7 +351,7 @@ describe("AutoDiscovery", function () { function () { const httpBackend = getHttpBackend(); httpBackend.when("GET", "/_matrix/client/versions").respond(200, { - not_matrix_versions: ["r0.0.1"], + not_matrix_versions: ["v1.1"], }); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { "m.homeserver": { @@ -388,7 +388,7 @@ describe("AutoDiscovery", function () { expect(req.path).toEqual("https://example.org/_matrix/client/versions"); }) .respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { "m.homeserver": { @@ -428,7 +428,7 @@ describe("AutoDiscovery", function () { expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions"); }) .respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { "m.homeserver": { @@ -469,7 +469,7 @@ describe("AutoDiscovery", function () { expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions"); }) .respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { "m.homeserver": { @@ -515,7 +515,7 @@ describe("AutoDiscovery", function () { expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions"); }) .respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { "m.homeserver": { @@ -560,7 +560,7 @@ describe("AutoDiscovery", function () { expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions"); }) .respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { "m.homeserver": { @@ -606,7 +606,7 @@ describe("AutoDiscovery", function () { expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions"); }) .respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); httpBackend.when("GET", "/_matrix/identity/v2").respond(404, {}); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { @@ -653,7 +653,7 @@ describe("AutoDiscovery", function () { expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions"); }) .respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); httpBackend.when("GET", "/_matrix/identity/v2").respond(500, {}); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { @@ -697,7 +697,7 @@ describe("AutoDiscovery", function () { expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions"); }) .respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); httpBackend .when("GET", "/_matrix/identity/v2") @@ -747,7 +747,7 @@ describe("AutoDiscovery", function () { expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions"); }) .respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); httpBackend .when("GET", "/_matrix/identity/v2") @@ -867,6 +867,37 @@ describe("AutoDiscovery", function () { ]); }); + it("should FAIL_ERROR for unsupported Matrix version", () => { + const httpBackend = getHttpBackend(); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + base_url: "https://example.org", + }, + }); + httpBackend.when("GET", "/_matrix/client/versions").respond(200, { + versions: ["r0.6.0"], + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: AutoDiscoveryAction.FAIL_ERROR, + error: AutoDiscovery.ERROR_HOMESERVER_TOO_OLD, + base_url: "https://example.org", + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + describe("m.authentication", () => { const homeserverName = "example.org"; const homeserverUrl = "https://chat.example.org/"; @@ -879,7 +910,7 @@ describe("AutoDiscovery", function () { beforeEach(() => { fetchMock.resetBehavior(); - fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["r0.0.1"] }); + fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["v1.1"] }); fetchMock.get("https://example.org/.well-known/matrix/client", { "m.homeserver": { diff --git a/spec/unit/content-repo.spec.ts b/spec/unit/content-repo.spec.ts index 2cbd361239e..91953945ac7 100644 --- a/spec/unit/content-repo.spec.ts +++ b/spec/unit/content-repo.spec.ts @@ -33,7 +33,7 @@ describe("ContentRepo", function () { it("should return a download URL if no width/height/resize are specified", function () { const mxcUri = "mxc://server.name/resourceid"; expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual( - baseUrl + "/_matrix/media/r0/download/server.name/resourceid", + baseUrl + "/_matrix/media/v3/download/server.name/resourceid", ); }); @@ -44,21 +44,21 @@ describe("ContentRepo", function () { it("should return a thumbnail URL if a width/height/resize is specified", function () { const mxcUri = "mxc://server.name/resourceid"; expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual( - baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" + "?width=32&height=64&method=crop", + baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32&height=64&method=crop", ); }); it("should put fragments from mxc:// URIs after any query parameters", function () { const mxcUri = "mxc://server.name/resourceid#automade"; expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual( - baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" + "?width=32#automade", + baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32#automade", ); }); it("should put fragments from mxc:// URIs at the end of the HTTP URI", function () { const mxcUri = "mxc://server.name/resourceid#automade"; expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual( - baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade", + baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade", ); }); }); diff --git a/spec/unit/http-api/__snapshots__/index.spec.ts.snap b/spec/unit/http-api/__snapshots__/index.spec.ts.snap index e6487ddeb0f..f60866aa238 100644 --- a/spec/unit/http-api/__snapshots__/index.spec.ts.snap +++ b/spec/unit/http-api/__snapshots__/index.spec.ts.snap @@ -6,6 +6,6 @@ exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] "params": { "access_token": "token", }, - "path": "/_matrix/media/r0/upload", + "path": "/_matrix/media/v3/upload", } `; diff --git a/spec/unit/http-api/index.spec.ts b/spec/unit/http-api/index.spec.ts index cbaa15aa417..668417ea9eb 100644 --- a/spec/unit/http-api/index.spec.ts +++ b/spec/unit/http-api/index.spec.ts @@ -84,7 +84,7 @@ describe("MatrixHttpApi", () => { upload = api.uploadContent({} as File); expect(xhr.open).toHaveBeenCalledWith( Method.Post, - baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token", + baseUrl.toLowerCase() + "/_matrix/media/v3/upload?access_token=token", ); expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization"); }); @@ -96,7 +96,7 @@ describe("MatrixHttpApi", () => { accessToken: "token", }); upload = api.uploadContent({} as File); - expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload"); + expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload"); expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token"); }); @@ -105,14 +105,14 @@ describe("MatrixHttpApi", () => { upload = api.uploadContent({} as File, { name: "name" }); expect(xhr.open).toHaveBeenCalledWith( Method.Post, - baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name", + baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name", ); }); it("should allow not sending the filename", () => { const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); upload = api.uploadContent({} as File, { name: "name", includeFilename: false }); - expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload"); + expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload"); }); it("should abort xhr when the upload is aborted", () => { diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 404f7fcd7fc..d90d3c9ed03 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -201,7 +201,7 @@ describe("MatrixClient", function () { if (path === KEEP_ALIVE_PATH && acceptKeepalives) { return Promise.resolve({ unstable_features: unstableFeatures, - versions: ["r0.6.0", "r0.6.1"], + versions: ["v1.1"], }); } const next = httpLookups.shift(); diff --git a/spec/unit/queueToDevice.spec.ts b/spec/unit/queueToDevice.spec.ts index c09d6968703..1099bcb82e8 100644 --- a/spec/unit/queueToDevice.spec.ts +++ b/spec/unit/queueToDevice.spec.ts @@ -195,7 +195,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st it("retries on retryImmediately()", async function () { httpBackend.when("GET", "/_matrix/client/versions").respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]); @@ -219,7 +219,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st it("retries on when client is started", async function () { httpBackend.when("GET", "/_matrix/client/versions").respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); await Promise.all([client.startClient(), httpBackend.flush("/_matrix/client/versions", 1, 20)]); @@ -243,7 +243,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st it("retries when a message is retried", async function () { httpBackend.when("GET", "/_matrix/client/versions").respond(200, { - versions: ["r0.0.1"], + versions: ["v1.1"], }); await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]); diff --git a/src/@types/requests.ts b/src/@types/requests.ts index bfab7fa0145..e67fb873b76 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -193,12 +193,6 @@ export interface IBindThreePidBody { sid: string; } -export interface IAddThreePidBody { - client_secret: string; - id_server: string; - sid: string; -} - export interface IRelationsRequestOpts { from?: string; to?: string; diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index b7a16f10702..a1ce6c09ab9 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -28,6 +28,7 @@ import { validateWellKnownAuthentication, } from "./oidc/validate"; import { OidcError } from "./oidc/error"; +import { MINIMUM_MATRIX_VERSION } from "./version-support"; // Dev note: Auto discovery is part of the spec. // See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery @@ -50,6 +51,9 @@ enum AutoDiscoveryError { InvalidIs = "Invalid identity server discovery response", MissingWellknown = "No .well-known JSON file found", InvalidJson = "Invalid JSON", + HomeserverTooOld = "The homeserver does not meet the minimum version requirements", + // TODO: Implement when Sydent supports the `/versions` endpoint - https://github.com/matrix-org/sydent/issues/424 + //IdentityServerTooOld = "The identity server does not meet the minimum version requirements", } interface AutoDiscoveryState { @@ -108,6 +112,8 @@ export class AutoDiscovery { public static readonly ERROR_INVALID_JSON = AutoDiscoveryError.InvalidJson; + public static readonly ERROR_HOMESERVER_TOO_OLD = AutoDiscoveryError.HomeserverTooOld; + public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError); /** @@ -199,7 +205,7 @@ export class AutoDiscovery { // Step 3: Make sure the homeserver URL points to a homeserver. const hsVersions = await this.fetchWellKnownObject(`${hsUrl}/_matrix/client/versions`); - if (!hsVersions?.raw?.["versions"]) { + if (!hsVersions || !Array.isArray(hsVersions.raw?.["versions"])) { logger.error("Invalid /versions response"); clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER; @@ -210,6 +216,18 @@ export class AutoDiscovery { return Promise.resolve(clientConfig); } + // Step 3.1: Non-spec check to ensure the server will actually work for us + if (!hsVersions.raw!["versions"].includes(MINIMUM_MATRIX_VERSION)) { + logger.error("Homeserver does not meet minimum version requirements"); + clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_HOMESERVER_TOO_OLD; + + // Supply the base_url to the caller because they may be ignoring liveliness + // errors, like this one. + clientConfig["m.homeserver"].base_url = hsUrl; + + return Promise.resolve(clientConfig); + } + // Step 4: Now that the homeserver looks valid, update our client config. clientConfig["m.homeserver"] = { state: AutoDiscovery.SUCCESS, diff --git a/src/client.ts b/src/client.ts index 750382a3b03..6b6fe2c227b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -133,7 +133,6 @@ import { IFilterResponse, ITagsResponse, IStatusResponse, - IAddThreePidBody, KnockRoomOpts, } from "./@types/requests"; import { @@ -1287,7 +1286,7 @@ export class MatrixClient extends TypedEventEmitter(Method.Post, "/room_keys/version", undefined, data, { - prefix: ClientPrefix.V3, - }); + const res = await this.http.authedRequest(Method.Post, "/room_keys/version", undefined, data); // We could assume everything's okay and enable directly, but this ensures // we run the same signature verification that will be used for future @@ -3929,7 +3926,7 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest(Method.Get, "/config", undefined, undefined, { - prefix: MediaPrefix.R0, + prefix: MediaPrefix.V3, }); } @@ -5184,7 +5181,7 @@ export class MatrixClient extends TypedEventEmitter { const postParams = Object.assign({}, params); - // If the HS supports separate add and bind, then requestToken endpoints - // don't need an IS as they are all validated by the HS directly. - if (!(await this.doesServerSupportSeparateAddAndBind()) && this.idBaseUrl) { - const idServerUrl = new URL(this.idBaseUrl); - postParams.id_server = idServerUrl.host; - - if (this.identityServer?.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) { - const identityAccessToken = await this.identityServer.getAccessToken(); - if (identityAccessToken) { - postParams.id_access_token = identityAccessToken; - } - } - } - return this.http.request(Method.Post, endpoint, undefined, postParams); } @@ -7387,78 +7370,6 @@ export class MatrixClient extends TypedEventEmitter { - const response = await this.getVersions(); - if (!response) return false; - - const versions = response["versions"]; - const unstableFeatures = response["unstable_features"]; - - return ( - (versions && versions.includes("r0.5.0")) || (unstableFeatures && unstableFeatures["m.lazy_load_members"]) - ); - } - - /** - * Query the server to see if the `id_server` parameter is required - * when registering with an 3pid, adding a 3pid or resetting password. - * @returns true if id_server parameter is required - */ - public async doesServerRequireIdServerParam(): Promise { - const response = await this.getVersions(); - if (!response) return true; - - const versions = response["versions"]; - - // Supporting r0.6.0 is the same as having the flag set to false - if (versions && versions.includes("r0.6.0")) { - return false; - } - - const unstableFeatures = response["unstable_features"]; - if (!unstableFeatures) return true; - if (unstableFeatures["m.require_identity_server"] === undefined) { - return true; - } else { - return unstableFeatures["m.require_identity_server"]; - } - } - - /** - * Query the server to see if the `id_access_token` parameter can be safely - * passed to the homeserver. Some homeservers may trigger errors if they are not - * prepared for the new parameter. - * @returns true if id_access_token can be sent - */ - public async doesServerAcceptIdentityAccessToken(): Promise { - const response = await this.getVersions(); - if (!response) return false; - - const versions = response["versions"]; - const unstableFeatures = response["unstable_features"]; - return (versions && versions.includes("r0.6.0")) || (unstableFeatures && unstableFeatures["m.id_access_token"]); - } - - /** - * Query the server to see if it supports separate 3PID add and bind functions. - * This affects the sequence of API calls clients should use for these operations, - * so it's helpful to be able to check for support. - * @returns true if separate functions are supported - */ - public async doesServerSupportSeparateAddAndBind(): Promise { - const response = await this.getVersions(); - if (!response) return false; - - const versions = response["versions"]; - const unstableFeatures = response["unstable_features"]; - - return versions?.includes("r0.6.0") || unstableFeatures?.["m.separate_add_and_bind"]; - } - /** * Query the server to see if it lists support for an unstable feature * in the /versions response @@ -7530,14 +7441,6 @@ export class MatrixClient extends TypedEventEmitter { - return this.isVersionSupported("r0.6.1"); - } - /** * Get if lazy loading members is being used. * @returns Whether or not members are lazy loaded by this client @@ -7922,18 +7825,6 @@ export class MatrixClient extends TypedEventEmitter { - return this.login("m.login.saml2", { - relay_state: relayState, - }); - } - /** * @param redirectUrl - The URL to redirect to after the HS * authenticates with CAS. @@ -7963,7 +7854,7 @@ export class MatrixClient extends TypedEventEmitter !i.id_access_token); - if ( - invitesNeedingToken.length > 0 && - this.identityServer?.getAccessToken && - (await this.doesServerAcceptIdentityAccessToken()) - ) { + if (invitesNeedingToken.length > 0 && this.identityServer?.getAccessToken) { const identityAccessToken = await this.identityServer.getAccessToken(); if (identityAccessToken) { for (const invite of invitesNeedingToken) { @@ -8465,30 +8348,6 @@ export class MatrixClient extends TypedEventEmitter { - const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", { - $networkId: networkId, - $roomId: roomId, - }); - return this.http.authedRequest(Method.Put, path, undefined, { visibility: visibility }); - } - /** * Query the user directory with a term matching user IDs, display names and domains. * @param term - the term with which to search. @@ -8571,31 +8430,10 @@ export class MatrixClient extends TypedEventEmitter { - const path = "/account/3pid"; - const data = { - threePidCreds: creds, - bind: bind, - }; - return this.http.authedRequest(Method.Post, path, undefined, data); - } - /** * Add a 3PID to your homeserver account. This API does not use an identity * server, as the homeserver is expected to handle 3PID ownership validation. * - * You can check whether a homeserver supports this API via - * `doesServerSupportSeparateAddAndBind`. - * * @param data - A object with 3PID validation data from having called * `account/3pid//requestToken` on the homeserver. * @returns Promise which resolves: to an empty object `{}` @@ -8603,8 +8441,7 @@ export class MatrixClient extends TypedEventEmitter { const path = "/account/3pid/add"; - const prefix = (await this.isVersionSupported("r0.6.0")) ? ClientPrefix.R0 : ClientPrefix.Unstable; - return this.http.authedRequest(Method.Post, path, undefined, data, { prefix }); + return this.http.authedRequest(Method.Post, path, undefined, data); } /** @@ -8612,9 +8449,6 @@ export class MatrixClient extends TypedEventEmitter/requestToken` on the identity server. It should also * contain `id_server` and `id_access_token` fields as well. @@ -8623,8 +8457,7 @@ export class MatrixClient extends TypedEventEmitter { const path = "/account/3pid/bind"; - const prefix = (await this.isVersionSupported("r0.6.0")) ? ClientPrefix.R0 : ClientPrefix.Unstable; - return this.http.authedRequest(Method.Post, path, undefined, data, { prefix }); + return this.http.authedRequest(Method.Post, path, undefined, data); } /** @@ -8649,8 +8482,7 @@ export class MatrixClient extends TypedEventEmitter { - return this.http.authedRequest(Method.Post, "/keys/signatures/upload", undefined, content, { - prefix: ClientPrefix.V3, - }); + return this.http.authedRequest(Method.Post, "/keys/signatures/upload", undefined, content); } /** diff --git a/src/content-repo.ts b/src/content-repo.ts index d3130aa722a..d83ab6dbd29 100644 --- a/src/content-repo.ts +++ b/src/content-repo.ts @@ -49,7 +49,7 @@ export function getHttpUriForMxc( } } let serverAndMediaId = mxc.slice(6); // strips mxc:// - let prefix = "/_matrix/media/r0/download/"; + let prefix = "/_matrix/media/v3/download/"; const params: Record = {}; if (width) { @@ -64,7 +64,7 @@ export function getHttpUriForMxc( if (Object.keys(params).length > 0) { // these are thumbnailing params so they probably want the // thumbnailing API... - prefix = "/_matrix/media/r0/thumbnail/"; + prefix = "/_matrix/media/v3/thumbnail/"; } const fragmentOffset = serverAndMediaId.indexOf("#"); diff --git a/src/http-api/index.ts b/src/http-api/index.ts index 1de1f847b27..4f9097665f8 100644 --- a/src/http-api/index.ts +++ b/src/http-api/index.ts @@ -110,7 +110,7 @@ export class MatrixHttpApi extends FetchHttpApi { }); }; - const url = this.getUrl("/upload", undefined, MediaPrefix.R0); + const url = this.getUrl("/upload", undefined, MediaPrefix.V3); if (includeFilename && fileName) { url.searchParams.set("filename", encodeURIComponent(fileName)); @@ -139,7 +139,7 @@ export class MatrixHttpApi extends FetchHttpApi { const headers: Record = { "Content-Type": contentType }; this.authedRequest(Method.Post, "/upload", queryParams, file, { - prefix: MediaPrefix.R0, + prefix: MediaPrefix.V3, headers, abortSignal: abortController.signal, }) @@ -182,7 +182,7 @@ export class MatrixHttpApi extends FetchHttpApi { public getContentUri(): IContentUri { return { base: this.opts.baseUrl, - path: MediaPrefix.R0 + "/upload", + path: MediaPrefix.V3 + "/upload", params: { access_token: this.opts.accessToken!, }, diff --git a/src/http-api/prefix.ts b/src/http-api/prefix.ts index f15b1ac1e77..5eee4672ae1 100644 --- a/src/http-api/prefix.ts +++ b/src/http-api/prefix.ts @@ -16,11 +16,7 @@ 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. + * A constant representing the URI path for Client-Server API endpoints versioned at v1. */ V1 = "/_matrix/client/v1", /** @@ -42,7 +38,11 @@ export enum IdentityPrefix { export enum MediaPrefix { /** - * URI path for the media repo API + * A constant representing the URI path for Client-Server API Media endpoints versioned at v1. + */ + V1 = "/_matrix/media/v3", + /** + * A constant representing the URI path for Client-Server API Media endpoints versioned at v3. */ - R0 = "/_matrix/media/r0", + V3 = "/_matrix/media/v3", } diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts index 3617ce1765f..d2a1fe087ab 100644 --- a/src/interactive-auth.ts +++ b/src/interactive-auth.ts @@ -345,10 +345,8 @@ export class InteractiveAuth { sid: this.emailSid, client_secret: this.clientSecret, }; - if (await this.matrixClient.doesServerRequireIdServerParam()) { - const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl()!); - creds.id_server = idServerParsedUrl.host; - } + const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl()!); + creds.id_server = idServerParsedUrl.host; authDict = { type: EMAIL_STAGE_TYPE, // TODO: Remove `threepid_creds` once servers support proper UIA diff --git a/src/sync.ts b/src/sync.ts index f2ec1b32b0f..8e8fde2199c 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -649,18 +649,11 @@ export class SyncApi { this.opts.lazyLoadMembers = false; } if (this.opts.lazyLoadMembers) { - debuglog("Checking server lazy load support..."); - const supported = await this.client.doesServerSupportLazyLoading(); - if (supported) { - debuglog("Enabling lazy load on sync filter..."); - if (!this.opts.filter) { - this.opts.filter = this.buildDefaultFilter(); - } - this.opts.filter.setLazyLoadMembers(true); - } else { - debuglog("LL: lazy loading requested but not supported " + "by server, so disabling"); - this.opts.lazyLoadMembers = false; + debuglog("Enabling lazy load on sync filter..."); + if (!this.opts.filter) { + this.opts.filter = this.buildDefaultFilter(); } + this.opts.filter.setLazyLoadMembers(true); } // need to vape the store when enabling LL and wasn't enabled before debuglog("Checking whether lazy loading has changed in store..."); diff --git a/src/version-support.ts b/src/version-support.ts new file mode 100644 index 00000000000..dd2d55ad9e2 --- /dev/null +++ b/src/version-support.ts @@ -0,0 +1,20 @@ +/* +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. +*/ + +/** + * The minimum Matrix specification version the js-sdk supports. + */ +export const MINIMUM_MATRIX_VERSION = "v1.1";