matrix-js-sdk 42.2.0 → 42.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/lib/client.d.ts +1 -2
  3. package/lib/client.d.ts.map +1 -1
  4. package/lib/client.js +16 -8
  5. package/lib/client.js.map +1 -1
  6. package/lib/http-api/errors.d.ts +8 -0
  7. package/lib/http-api/errors.d.ts.map +1 -1
  8. package/lib/http-api/errors.js +12 -0
  9. package/lib/http-api/errors.js.map +1 -1
  10. package/lib/matrixrtc/MatrixRTCSession.d.ts +5 -0
  11. package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
  12. package/lib/matrixrtc/MatrixRTCSession.js +2 -1
  13. package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
  14. package/lib/oauth/authorize.d.ts +2 -4
  15. package/lib/oauth/authorize.d.ts.map +1 -1
  16. package/lib/oauth/authorize.js.map +1 -1
  17. package/lib/oauth/error.d.ts +44 -0
  18. package/lib/oauth/error.d.ts.map +1 -1
  19. package/lib/oauth/error.js +53 -0
  20. package/lib/oauth/error.js.map +1 -1
  21. package/lib/oauth/index.d.ts.map +1 -1
  22. package/lib/oauth/index.js +19 -2
  23. package/lib/oauth/index.js.map +1 -1
  24. package/lib/oauth/tokenRefresher.d.ts.map +1 -1
  25. package/lib/oauth/tokenRefresher.js +4 -2
  26. package/lib/oauth/tokenRefresher.js.map +1 -1
  27. package/lib/secret-storage.d.ts +1 -1
  28. package/lib/secret-storage.d.ts.map +1 -1
  29. package/lib/secret-storage.js +9 -32
  30. package/lib/secret-storage.js.map +1 -1
  31. package/lib/sliding-sync-sdk.d.ts.map +1 -1
  32. package/lib/sliding-sync-sdk.js +54 -1
  33. package/lib/sliding-sync-sdk.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/client.ts +19 -11
  36. package/src/http-api/errors.ts +16 -0
  37. package/src/matrixrtc/MatrixRTCSession.ts +8 -1
  38. package/src/oauth/authorize.ts +2 -5
  39. package/src/oauth/error.ts +69 -0
  40. package/src/oauth/index.ts +19 -2
  41. package/src/oauth/tokenRefresher.ts +9 -2
  42. package/src/secret-storage.ts +8 -31
  43. package/src/sliding-sync-sdk.ts +74 -0
package/src/client.ts CHANGED
@@ -7379,10 +7379,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
7379
7379
  * @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md
7380
7380
  * @param userId The user ID to fetch the profile of.
7381
7381
  * @param key The key of the property to fetch.
7382
- * @returns The property value.
7382
+ * @returns The property value, or `undefined` if the key was not set OR the profile could not be found.
7383
7383
  *
7384
7384
  * @throws An error if the server does not support MSC4133.
7385
- * @throws A M_NOT_FOUND error if the key was not set OR the profile could not be found.
7386
7385
  */
7387
7386
  public async getExtendedProfileProperty(userId: string, key: string): Promise<unknown> {
7388
7387
  if (!(await this.doesServerSupportExtendedProfiles())) {
@@ -7394,15 +7393,24 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
7394
7393
  if (storedProfile?.[key] !== undefined) {
7395
7394
  return storedProfile[key];
7396
7395
  }
7397
- const profile = (await this.http.authedRequest(
7398
- Method.Get,
7399
- utils.encodeUri("/profile/$userId/$key", { $userId: userId, $key: key }),
7400
- undefined,
7401
- undefined,
7402
- {
7403
- prefix: await this.getExtendedProfileRequestPrefix(),
7404
- },
7405
- )) as SyncUserProfile;
7396
+ let profile: SyncUserProfile;
7397
+ try {
7398
+ profile = (await this.http.authedRequest(
7399
+ Method.Get,
7400
+ utils.encodeUri("/profile/$userId/$key", { $userId: userId, $key: key }),
7401
+ undefined,
7402
+ undefined,
7403
+ {
7404
+ prefix: await this.getExtendedProfileRequestPrefix(),
7405
+ },
7406
+ )) as SyncUserProfile;
7407
+ } catch (e) {
7408
+ if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_NOT_FOUND") {
7409
+ // The key is not set on the profile (or the profile does not exist).
7410
+ return undefined;
7411
+ }
7412
+ throw e;
7413
+ }
7406
7414
 
7407
7415
  // write through to the cache
7408
7416
  await this.store.storeUserProfiles(
@@ -19,6 +19,7 @@ import { type IMatrixApiError as IWidgetMatrixError } from "matrix-widget-api";
19
19
  import { type IUsageLimit } from "../@types/partials.ts";
20
20
  import { type MatrixEvent } from "../models/event.ts";
21
21
  import { NamespacedValue } from "../NamespacedValue.ts";
22
+ import { hasRequiredStringProperty, isRecord } from "../@types/type-guards.ts";
22
23
 
23
24
  interface IErrorJson extends Partial<IUsageLimit> {
24
25
  [key: string]: any; // extensible
@@ -80,6 +81,21 @@ export class HTTPError extends Error {
80
81
  }
81
82
  }
82
83
 
84
+ /**
85
+ * Check if the given (JSON-parsed) response body looks like a Matrix error
86
+ * response as specified in https://spec.matrix.org/v1.19/client-server-api/#standard-error-response
87
+ *
88
+ * @param response - the parsed response body to check
89
+ * @returns whether the response is a valid {@link MatrixError}
90
+ */
91
+ export function isMatrixErrorResponse(response: unknown): response is MatrixError {
92
+ return (
93
+ isRecord(response) &&
94
+ hasRequiredStringProperty(response, "error") &&
95
+ hasRequiredStringProperty(response, "errcode")
96
+ );
97
+ }
98
+
83
99
  export class MatrixError extends HTTPError {
84
100
  // The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
85
101
  public readonly errcode?: string;
@@ -96,6 +96,12 @@ export interface SessionConfig {
96
96
  * Determines the kind of call this will be.
97
97
  */
98
98
  callIntent?: RTCCallIntent;
99
+
100
+ /**
101
+ * How long (in milliseconds) the callee's client should keep ringing/waiting for an
102
+ * answer before the sender gives up and the call notification is considered timed out.
103
+ */
104
+ notificationLifetimeMs?: number;
99
105
  }
100
106
 
101
107
  // The names follow these principles:
@@ -697,6 +703,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
697
703
  notificationType: RTCNotificationType,
698
704
  callIntent?: RTCCallIntent,
699
705
  ): void {
706
+ const lifetime = this.joinConfig?.notificationLifetimeMs ?? 90_000;
700
707
  const sendNotificationEvent = async (): Promise<{
701
708
  response: ISendEventResponse;
702
709
  content: IRTCNotificationContent;
@@ -709,7 +716,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
709
716
  rel_type: RelationType.Reference,
710
717
  },
711
718
  "sender_ts": Date.now(),
712
- "lifetime": 30_000, // 30 seconds
719
+ "lifetime": lifetime,
713
720
  };
714
721
  if (callIntent) {
715
722
  content["m.call.intent"] = callIntent;
@@ -15,7 +15,7 @@ limitations under the License.
15
15
  */
16
16
 
17
17
  import { secureRandomString } from "../randomstring.ts";
18
- import { OAuth2Error } from "./error.ts";
18
+ import { OAuth2Error, type OAuth2ErrorResponse } from "./error.ts";
19
19
  import { type ValidatedAuthMetadata } from "./discover.ts";
20
20
  import {
21
21
  hasOptionalNumberProperty,
@@ -127,10 +127,7 @@ export function isValidDeviceAccessTokenResponse(response: unknown): response is
127
127
  /**
128
128
  * Error from the OAuth2 token endpoint when exchanging a token for grant_type device_code.
129
129
  */
130
- export interface DeviceAccessTokenError {
131
- error: string;
132
- error_description?: string;
133
- error_uri?: string;
130
+ export interface DeviceAccessTokenError extends OAuth2ErrorResponse {
134
131
  session_state?: string;
135
132
  }
136
133
 
@@ -14,6 +14,9 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { hasOptionalStringProperty, hasRequiredStringProperty, isRecord } from "../@types/type-guards.ts";
18
+ import { HTTPError } from "../http-api/errors.ts";
19
+
17
20
  /**
18
21
  * Errors expected to be encountered during OAuth2 discovery, client registration, and authentication.
19
22
  * Not intended to be displayed directly to the user.
@@ -32,3 +35,69 @@ export enum OAuth2Error {
32
35
  RevokeTokenFailed = "Failed to revoke token",
33
36
  DeviceAuthorizationGrantFailed = "Failed to perform device authorization grant",
34
37
  }
38
+
39
+ /**
40
+ * An error response from an OAuth 2.0 endpoint,
41
+ * as specified in https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
42
+ */
43
+ export interface OAuth2ErrorResponse {
44
+ /** A single ASCII error code, e.g. `invalid_grant`. */
45
+ error: string;
46
+ /** Human-readable ASCII text providing additional information about the error. */
47
+ error_description?: string;
48
+ /** A URI identifying a human-readable web page with information about the error. */
49
+ error_uri?: string;
50
+ }
51
+
52
+ /**
53
+ * Check whether the given (JSON-parsed) response body is an OAuth 2.0 error response
54
+ * as specified in https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
55
+ * @param response - the parsed response body to check
56
+ * @returns whether the response is a valid {@link OAuth2ErrorResponse}
57
+ */
58
+ export function isOAuth2ErrorResponse(response: unknown): response is OAuth2ErrorResponse {
59
+ return (
60
+ isRecord(response) &&
61
+ hasRequiredStringProperty(response, "error") &&
62
+ hasOptionalStringProperty(response, "error_description") &&
63
+ hasOptionalStringProperty(response, "error_uri")
64
+ );
65
+ }
66
+
67
+ /**
68
+ * An error thrown when a request to an OAuth 2.0 endpoint fails with a body matching the error
69
+ * response format specified in [RFC 6749 section 5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2).
70
+ */
71
+ export class OAuth2HTTPError extends HTTPError implements OAuth2ErrorResponse {
72
+ /**
73
+ * RFC 6749 section 5.2 error code, e.g. `invalid_grant`
74
+ *
75
+ * IANA matains a registry of valid values at
76
+ * https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#extensions-error
77
+ */
78
+ public error: string;
79
+
80
+ /**
81
+ * RFC 6749 section 5.2 human-readable ASCII text providing additional information about the error.
82
+ * This field is optional and may be omitted by the endpoint.
83
+ */
84
+ public error_description?: string;
85
+
86
+ /**
87
+ * RFC 6749 section 5.2 URI identifying a human-readable web page with information about the error.
88
+ * This field is optional and may be omitted by the endpoint.
89
+ */
90
+ public error_uri?: string;
91
+
92
+ public constructor(
93
+ msg: string,
94
+ httpStatus: number | undefined,
95
+ httpHeaders: Headers | undefined,
96
+ { error, error_description, error_uri }: OAuth2ErrorResponse,
97
+ ) {
98
+ super(msg, httpStatus, httpHeaders);
99
+ this.error = error;
100
+ this.error_description = error_description;
101
+ this.error_uri = error_uri;
102
+ }
103
+ }
@@ -34,9 +34,9 @@ import {
34
34
  } from "./register.ts";
35
35
  import { encodeUnpaddedBase64Url } from "../base64.ts";
36
36
  import { sha256 } from "../digest.ts";
37
- import { HTTPError, Method } from "../http-api/index.ts";
37
+ import { HTTPError, isMatrixErrorResponse, MatrixError, Method } from "../http-api/index.ts";
38
38
  import { logger } from "../logger.ts";
39
- import { OAuth2Error } from "./error.ts";
39
+ import { isOAuth2ErrorResponse, OAuth2Error, OAuth2HTTPError } from "./error.ts";
40
40
  import { secureRandomString } from "../randomstring.ts";
41
41
  import { type NonEmptyArray } from "../@types/common.ts";
42
42
 
@@ -289,6 +289,23 @@ export class OAuth2 {
289
289
  });
290
290
 
291
291
  if (res.status >= 400) {
292
+ let body: unknown;
293
+ try {
294
+ body = await res.json();
295
+ } catch {
296
+ // The endpoint didn't give us a JSON body, so we can't determine the error type. We'll throw a generic
297
+ // HTTPError below.
298
+ }
299
+ // Because the Matrix C-S API error response format is so similar to the OAuth 2.0 error response format
300
+ // the ordering of these checks is important. We want to check for a Matrix error response first, and only
301
+ // if it isn't one do we check for an OAuth 2.0 error response.
302
+ // This essentially relies on `errcode` not being present in an OAuth 2.0 error response.
303
+ if (isMatrixErrorResponse(body)) {
304
+ throw new MatrixError(body, res.status, undefined, undefined, res.headers);
305
+ }
306
+ if (isOAuth2ErrorResponse(body)) {
307
+ throw new OAuth2HTTPError(error, res.status, res.headers, body);
308
+ }
292
309
  throw new HTTPError(error, res.status, res.headers);
293
310
  }
294
311
 
@@ -15,6 +15,7 @@ limitations under the License.
15
15
  */
16
16
 
17
17
  import { type AccessTokens, HTTPError, type TokenRefreshFunction, TokenRefreshLogoutError } from "../http-api/index.ts";
18
+ import { OAuth2HTTPError } from "./error.ts";
18
19
  import { type OAuth2 } from "./index.ts";
19
20
 
20
21
  /**
@@ -54,8 +55,14 @@ export class TokenRefresher {
54
55
  };
55
56
 
56
57
  private shouldLogoutOnError(error: HTTPError): boolean {
57
- // As per https://spec.matrix.org/v1.18/client-server-api/#refresh-token-grant
58
- return typeof error.httpStatus === "number" && error.httpStatus < 500 && error.httpStatus >= 400;
58
+ // Treat as logout as per https://spec.matrix.org/v1.18/client-server-api/#refresh-token-grant
59
+ // after making sure it is an RFC 6749 section 5.2 error response
60
+ return (
61
+ error instanceof OAuth2HTTPError &&
62
+ typeof error.httpStatus === "number" &&
63
+ error.httpStatus < 500 &&
64
+ error.httpStatus >= 400
65
+ );
59
66
  }
60
67
 
61
68
  private async getNewTokens(refreshToken: string): Promise<AccessTokens> {
@@ -21,8 +21,7 @@ limitations under the License.
21
21
  */
22
22
 
23
23
  import { type TypedEventEmitter } from "./models/typed-event-emitter.ts";
24
- import { ClientEvent, type ClientEventHandlerMap } from "./client.ts";
25
- import { type MatrixEvent } from "./models/event.ts";
24
+ import { type ClientEvent, type ClientEventHandlerMap } from "./client.ts";
26
25
  import { secureRandomString } from "./randomstring.ts";
27
26
  import { logger } from "./logger.ts";
28
27
  import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
@@ -370,35 +369,13 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
370
369
  /**
371
370
  * Implementation of {@link ServerSideSecretStorage#setDefaultKeyId}.
372
371
  */
373
- public setDefaultKeyId(keyId: string | null): Promise<void> {
374
- return new Promise<void>((resolve, reject) => {
375
- const listener = (ev: MatrixEvent): void => {
376
- if (ev.getType() !== "m.secret_storage.default_key") {
377
- // Different account data item
378
- return;
379
- }
380
-
381
- // If keyId === null, the content should be an empty object.
382
- // Otherwise, the `key` in the content object should match keyId.
383
- const content = ev.getContent();
384
- const isSameKey = keyId === null ? Object.keys(content).length === 0 : content.key === keyId;
385
- if (isSameKey) {
386
- this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
387
- resolve();
388
- }
389
- };
390
- this.accountDataAdapter.on(ClientEvent.AccountData, listener);
391
-
392
- // The spec [1] says that the value of the account data entry should be an object with a `key` property.
393
- // It doesn't specify how to delete the default key; we do it by setting the account data to an empty object.
394
- //
395
- // [1]: https://spec.matrix.org/v1.13/client-server-api/#key-storage
396
- const newValue: Record<string, never> | { key: string } = keyId === null ? {} : { key: keyId };
397
- this.accountDataAdapter.setAccountData("m.secret_storage.default_key", newValue).catch((e) => {
398
- this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
399
- reject(e);
400
- });
401
- });
372
+ public async setDefaultKeyId(keyId: string | null): Promise<void> {
373
+ // The spec [1] says that the value of the account data entry should be an object with a `key` property.
374
+ // It doesn't specify how to delete the default key; we do it by setting the account data to an empty object.
375
+ //
376
+ // [1]: https://spec.matrix.org/v1.13/client-server-api/#key-storage
377
+ const newValue: Record<string, never> | { key: string } = keyId === null ? {} : { key: keyId };
378
+ await this.accountDataAdapter.setAccountData("m.secret_storage.default_key", newValue);
402
379
  }
403
380
 
404
381
  /**
@@ -35,6 +35,8 @@ import {
35
35
  type IMinimalEvent,
36
36
  type IRoomEvent,
37
37
  type IStateEvent,
38
+ type IStickyEvent,
39
+ type IStickyStateEvent,
38
40
  type IStrippedState,
39
41
  type ISyncResponse,
40
42
  type ReceivedToDeviceMessage,
@@ -311,6 +313,73 @@ class ExtensionReceipts implements Extension<ExtensionReceiptsRequest, Extension
311
313
  }
312
314
  }
313
315
 
316
+ type ExtensionStickyEventsRequest = {
317
+ enabled: boolean;
318
+ /** Max events per response; the server may return fewer. */
319
+ limit?: number;
320
+ /** The `next_batch` of the previous response. */
321
+ since?: string;
322
+ };
323
+
324
+ type ExtensionStickyEventsResponse = {
325
+ /** Only sent when there were changes. */
326
+ next_batch?: string;
327
+ rooms?: Record<string, { events: Array<IStickyEvent | IStickyStateEvent> }>;
328
+ };
329
+
330
+ /**
331
+ * Delivers sticky events (MSC4354) over sliding sync.
332
+ * https://github.com/matrix-org/matrix-spec-proposals/pull/4480
333
+ *
334
+ * Sticky events expire after a duration instead of living in the timeline forever, and the server
335
+ * re-sends the unexpired ones (e.g. on join) so late joiners still see them.
336
+ *
337
+ * The server sends them for every room matched by a list or subscription, even rooms currently
338
+ * outside the list window. Sticky events already in a room's timeline are excluded here, so
339
+ * `processRoomData` picks those up separately.
340
+ */
341
+ class ExtensionStickyEvents implements Extension<ExtensionStickyEventsRequest, ExtensionStickyEventsResponse> {
342
+ private nextBatch?: string;
343
+
344
+ public constructor(private readonly client: MatrixClient) {}
345
+
346
+ public name(): string {
347
+ // Keeps MSC4354's number, as the extension was originally specified there.
348
+ return "org.matrix.msc4354.sticky_events";
349
+ }
350
+
351
+ public when(): ExtensionState {
352
+ // Sticky events are stored on a Room, so the room has to exist first.
353
+ return ExtensionState.PostProcess;
354
+ }
355
+
356
+ public async onRequest(isInitial: boolean): Promise<ExtensionStickyEventsRequest> {
357
+ return {
358
+ enabled: true,
359
+ limit: 100,
360
+ // Undefined until the first response, which asks for all unexpired sticky events.
361
+ since: this.nextBatch,
362
+ };
363
+ }
364
+
365
+ public async onResponse(data: ExtensionStickyEventsResponse): Promise<void> {
366
+ for (const [roomId, roomData] of Object.entries(data?.rooms ?? {})) {
367
+ const room = this.client.getRoom(roomId);
368
+ if (!room) {
369
+ // Dropping is safe: unexpired sticky events are re-sent once we know the room.
370
+ logger.debug(`Ignoring sticky events for unknown room ${roomId}`);
371
+ continue;
372
+ }
373
+ room._unstable_addStickyEvents(mapEvents(this.client, roomId, roomData.events ?? []));
374
+ }
375
+
376
+ // next_batch is only returned when there were changes, and must be echoed back as `since`.
377
+ if (data?.next_batch) {
378
+ this.nextBatch = data.next_batch;
379
+ }
380
+ }
381
+ }
382
+
314
383
  /**
315
384
  * A copy of SyncApi such that it can be used as a drop-in replacement for sync v2. For the actual
316
385
  * sliding sync API, see sliding-sync.ts or the class SlidingSync.
@@ -344,6 +413,7 @@ export class SlidingSyncSdk {
344
413
  new ExtensionAccountData(this.client),
345
414
  new ExtensionTyping(this.client),
346
415
  new ExtensionReceipts(this.client),
416
+ new ExtensionStickyEvents(this.client),
347
417
  ];
348
418
  if (this.syncOpts.cryptoCallbacks) {
349
419
  extensions.push(new ExtensionE2EE(this.syncOpts.cryptoCallbacks));
@@ -705,6 +775,10 @@ export class SlidingSyncSdk {
705
775
 
706
776
  room.setMSC4186SummaryData(roomData.heroes, roomData.joined_count, roomData.invited_count);
707
777
 
778
+ // The MSC4480 extension excludes sticky events already present in the timeline, so we have
779
+ // to pick those up here. See ExtensionStickyEvents for the rest.
780
+ room._unstable_addStickyEvents(timelineEvents.filter((e) => e.unstableStickyInfo !== undefined));
781
+
708
782
  room.recalculate();
709
783
  if (roomData.initial) {
710
784
  client.store.storeRoom(room);