feat: complete Ancestor quote bot with production-ready Docker support
This commit is contained in:
29
node_modules/matrix-js-sdk/src/@types/AESEncryptedSecretStoragePayload.ts
generated
vendored
29
node_modules/matrix-js-sdk/src/@types/AESEncryptedSecretStoragePayload.ts
generated
vendored
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An AES-encrypted secret storage payload.
|
||||
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2-1
|
||||
*/
|
||||
export interface AESEncryptedSecretStoragePayload {
|
||||
[key: string]: any; // extensible
|
||||
/** the initialization vector in base64 */
|
||||
iv: string;
|
||||
/** the ciphertext in base64 */
|
||||
ciphertext: string;
|
||||
/** the HMAC in base64 */
|
||||
mac: string;
|
||||
}
|
||||
8
node_modules/matrix-js-sdk/src/@types/PushRules.ts
generated
vendored
8
node_modules/matrix-js-sdk/src/@types/PushRules.ts
generated
vendored
@@ -14,6 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export enum PushRuleActionName {
|
||||
DontNotify = "dont_notify",
|
||||
Notify = "notify",
|
||||
@@ -99,7 +102,8 @@ export interface IRoomMemberCountCondition extends IPushRuleCondition<ConditionK
|
||||
is: AnyMemberCountCondition;
|
||||
}
|
||||
|
||||
export interface ISenderNotificationPermissionCondition extends IPushRuleCondition<ConditionKind.SenderNotificationPermission> {
|
||||
export interface ISenderNotificationPermissionCondition
|
||||
extends IPushRuleCondition<ConditionKind.SenderNotificationPermission> {
|
||||
key: string;
|
||||
}
|
||||
|
||||
@@ -201,3 +205,5 @@ export interface IPusher {
|
||||
export interface IPusherRequest extends Omit<IPusher, "device_id" | "org.matrix.msc3881.device_id"> {
|
||||
append?: boolean;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
33
node_modules/matrix-js-sdk/src/@types/auth.ts
generated
vendored
33
node_modules/matrix-js-sdk/src/@types/auth.ts
generated
vendored
@@ -14,8 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue.ts";
|
||||
import { type IClientWellKnown } from "../client.ts";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IClientWellKnown } from "../client";
|
||||
|
||||
// disable lint because these are wire responses
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
/**
|
||||
* Represents a response to the CSAPI `/refresh` endpoint.
|
||||
@@ -26,6 +29,8 @@ export interface IRefreshTokenResponse {
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
/**
|
||||
* Response to GET login flows as per https://spec.matrix.org/v1.3/client-server-api/#get_matrixclientv3login
|
||||
*/
|
||||
@@ -43,8 +48,8 @@ export interface IPasswordFlow extends ILoginFlow {
|
||||
type: "m.login.password";
|
||||
}
|
||||
|
||||
export const OAUTH_AWARE_PREFERRED_FLOW_FIELD = new UnstableValue(
|
||||
"oauth_aware_preferred",
|
||||
export const DELEGATED_OIDC_COMPATIBILITY = new UnstableValue(
|
||||
"delegated_oidc_compatibility",
|
||||
"org.matrix.msc3824.delegated_oidc_compatibility",
|
||||
);
|
||||
|
||||
@@ -53,9 +58,10 @@ export const OAUTH_AWARE_PREFERRED_FLOW_FIELD = new UnstableValue(
|
||||
*/
|
||||
export interface ISSOFlow extends ILoginFlow {
|
||||
type: "m.login.sso" | "m.login.cas";
|
||||
// eslint-disable-next-line camelcase
|
||||
identity_providers?: IIdentityProvider[];
|
||||
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.name]?: boolean;
|
||||
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.altName]?: boolean;
|
||||
[DELEGATED_OIDC_COMPATIBILITY.name]?: boolean;
|
||||
[DELEGATED_OIDC_COMPATIBILITY.altName]?: boolean;
|
||||
}
|
||||
|
||||
export enum IdentityProviderBrand {
|
||||
@@ -141,6 +147,11 @@ export interface LoginRequest {
|
||||
* The login type being used.
|
||||
*/
|
||||
type: "m.login.password" | "m.login.token" | string;
|
||||
/**
|
||||
* Third-party identifier for the user.
|
||||
* @deprecated in favour of `identifier`.
|
||||
*/
|
||||
address?: string;
|
||||
/**
|
||||
* ID of the client device.
|
||||
* If this does not correspond to a known client device, a new device will be created.
|
||||
@@ -232,13 +243,21 @@ export interface LoginResponse {
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a successful `m.login.token` issuance request as per https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv1loginget_token
|
||||
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
|
||||
* `m.login.token` issuance request.
|
||||
* Note that this is UNSTABLE and subject to breaking changes without notice.
|
||||
*/
|
||||
export interface LoginTokenPostResponse {
|
||||
/**
|
||||
* The token to use with `m.login.token` to authenticate.
|
||||
*/
|
||||
login_token: string;
|
||||
/**
|
||||
* Expiration in seconds.
|
||||
*
|
||||
* @deprecated this is only provided for compatibility with original revision of the MSC.
|
||||
*/
|
||||
expires_in: number;
|
||||
/**
|
||||
* Expiration in milliseconds.
|
||||
*/
|
||||
|
||||
8
node_modules/matrix-js-sdk/src/@types/beacon.ts
generated
vendored
8
node_modules/matrix-js-sdk/src/@types/beacon.ts
generated
vendored
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type RelatesToRelationship, type REFERENCE_RELATION } from "./extensible_events.ts";
|
||||
import { UnstableValue } from "../NamespacedValue.ts";
|
||||
import { type MAssetEvent, type MLocationEvent, type MTimestampEvent } from "./location.ts";
|
||||
import { RelatesToRelationship, REFERENCE_RELATION } from "./extensible_events";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
|
||||
|
||||
/**
|
||||
* Beacon info and beacon event types as described in MSC3672
|
||||
@@ -30,7 +30,7 @@ import { type MAssetEvent, type MLocationEvent, type MTimestampEvent } from "./l
|
||||
* 2. a user can have an arbitrary number of beacon_info events
|
||||
*
|
||||
* 1. is achieved by setting the state_key to the owners mxid.
|
||||
* Event keys in room state are a combination of `typestate_key`.
|
||||
* Event keys in room state are a combination of `type` + `state_key`.
|
||||
* To achieve an arbitrary number of only owner-writable state events
|
||||
* we introduce a variable suffix to the event type
|
||||
*
|
||||
|
||||
24
node_modules/matrix-js-sdk/src/@types/common.ts
generated
vendored
24
node_modules/matrix-js-sdk/src/@types/common.ts
generated
vendored
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 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 type NonEmptyArray<T> = [T, ...T[]];
|
||||
|
||||
// Based on https://stackoverflow.com/a/53229857/3532235
|
||||
export type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
export type XOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
||||
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
export type EmptyObject = Record<PropertyKey, never>;
|
||||
50
node_modules/matrix-js-sdk/src/@types/crypto.ts
generated
vendored
50
node_modules/matrix-js-sdk/src/@types/crypto.ts
generated
vendored
@@ -14,19 +14,49 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ISignatures } from "./signed.ts";
|
||||
import type { EventDecryptionResult } from "../common-crypto/CryptoBackend.ts";
|
||||
import type { IClearEvent } from "../models/event";
|
||||
import type { ISignatures } from "./signed";
|
||||
|
||||
// Backwards compatible re-export
|
||||
/** @deprecated This is an internal type and should not be used. */
|
||||
type IEventDecryptionResult = EventDecryptionResult;
|
||||
export type { IEventDecryptionResult };
|
||||
export type OlmGroupSessionExtraData = {
|
||||
untrusted?: boolean;
|
||||
sharedHistory?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The result of a (successful) call to {@link Crypto.decryptEvent}
|
||||
*/
|
||||
export interface IEventDecryptionResult {
|
||||
/**
|
||||
* The plaintext payload for the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
|
||||
*/
|
||||
clearEvent: IClearEvent;
|
||||
/**
|
||||
* List of curve25519 keys involved in telling us about the senderCurve25519Key and claimedEd25519Key.
|
||||
* See {@link MatrixEvent#getForwardingCurve25519KeyChain}.
|
||||
*/
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
/**
|
||||
* Key owned by the sender of this event. See {@link MatrixEvent#getSenderKey}.
|
||||
*/
|
||||
senderCurve25519Key?: string;
|
||||
/**
|
||||
* ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}.
|
||||
*/
|
||||
claimedEd25519Key?: string;
|
||||
untrusted?: boolean;
|
||||
/**
|
||||
* The sender doesn't authorize the unverified devices to decrypt his messages
|
||||
*/
|
||||
encryptedDisabledForUnverifiedDevices?: boolean;
|
||||
}
|
||||
|
||||
interface Extensible {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** The result of a call to {@link crypto-api!CryptoApi.exportRoomKeys} */
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
/** The result of a call to {@link MatrixClient.exportRoomKeys} */
|
||||
export interface IMegolmSessionData extends Extensible {
|
||||
/** Sender's Curve25519 device key */
|
||||
sender_key: string;
|
||||
@@ -44,14 +74,16 @@ export interface IMegolmSessionData extends Extensible {
|
||||
untrusted?: boolean;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
/** the type of the `device_keys` parameter on `/_matrix/client/v3/keys/upload`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3keysupload
|
||||
*/
|
||||
export interface IDeviceKeys {
|
||||
algorithms: Array<string>;
|
||||
device_id: string;
|
||||
user_id: string;
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
keys: Record<string, string>;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
243
node_modules/matrix-js-sdk/src/@types/event.ts
generated
vendored
243
node_modules/matrix-js-sdk/src/@types/event.ts
generated
vendored
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2020-2026 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020 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.
|
||||
@@ -14,59 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type EitherAnd } from "matrix-events-sdk";
|
||||
|
||||
import { NamespacedValue, UnstableValue } from "../NamespacedValue.ts";
|
||||
import {
|
||||
type PolicyRuleEventContent,
|
||||
type RoomAvatarEventContent,
|
||||
type RoomCanonicalAliasEventContent,
|
||||
type RoomCreateEventContent,
|
||||
type RoomEncryptionEventContent,
|
||||
type RoomGuestAccessEventContent,
|
||||
type RoomHistoryVisibilityEventContent,
|
||||
type RoomJoinRulesEventContent,
|
||||
type RoomMemberEventContent,
|
||||
type RoomNameEventContent,
|
||||
type RoomPinnedEventsEventContent,
|
||||
type RoomPolicyContent,
|
||||
type RoomPowerLevelsEventContent,
|
||||
type RoomServerAclEventContent,
|
||||
type RoomThirdPartyInviteEventContent,
|
||||
type RoomTombstoneEventContent,
|
||||
type RoomTopicEventContent,
|
||||
type SpaceChildEventContent,
|
||||
type SpaceParentEventContent,
|
||||
} from "./state_events.ts";
|
||||
import { type IGroupCallRoomMemberState, type IGroupCallRoomState } from "../webrtc/groupCall.ts";
|
||||
import { type MSC3089EventContent } from "../models/MSC3089Branch.ts";
|
||||
import { type M_BEACON, type M_BEACON_INFO, type MBeaconEventContent, type MBeaconInfoEventContent } from "./beacon.ts";
|
||||
import { type EmptyObject } from "./common.ts";
|
||||
import { type ReactionEventContent, type RoomMessageEventContent, type StickerEventContent } from "./events.ts";
|
||||
import {
|
||||
type MCallAnswer,
|
||||
type MCallBase,
|
||||
type MCallCandidates,
|
||||
type MCallHangupReject,
|
||||
type MCallInviteNegotiate,
|
||||
type MCallReplacesEvent,
|
||||
type MCallSelectAnswer,
|
||||
type SDPStreamMetadata,
|
||||
} from "../webrtc/callEventTypes.ts";
|
||||
import {
|
||||
type IRTCNotificationContent,
|
||||
type IRTCDeclineContent,
|
||||
type EncryptionKeysEventContent,
|
||||
type ICallNotifyContent,
|
||||
type RtcSlotEventContent,
|
||||
} from "../matrixrtc/types.ts";
|
||||
import { type M_POLL_END, type M_POLL_START, type PollEndEventContent, type PollStartEventContent } from "./polls.ts";
|
||||
import { type RtcMembershipData, type SessionMembershipData } from "../matrixrtc/membershipData/index.ts";
|
||||
import { type LocalNotificationSettings } from "./local_notifications.ts";
|
||||
import { type IPushRules } from "./PushRules.ts";
|
||||
import { type SecretInfo, type SecretStorageKeyDescription } from "../secret-storage.ts";
|
||||
import { type POLICIES_ACCOUNT_EVENT_TYPE } from "../models/invites-ignorer-types.ts";
|
||||
import type { ROOM_RETENTION_TYPE, RoomRetentionContent } from "./retention.ts";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
|
||||
export enum EventType {
|
||||
// Room state events
|
||||
@@ -87,11 +35,6 @@ export enum EventType {
|
||||
RoomTombstone = "m.room.tombstone",
|
||||
RoomPredecessor = "org.matrix.msc3946.room_predecessor",
|
||||
|
||||
// Moderation policy lists
|
||||
PolicyRuleUser = "m.policy.rule.user",
|
||||
PolicyRuleRoom = "m.policy.rule.room",
|
||||
PolicyRuleServer = "m.policy.rule.server",
|
||||
|
||||
SpaceChild = "m.space.child",
|
||||
SpaceParent = "m.space.parent",
|
||||
|
||||
@@ -112,7 +55,6 @@ export enum EventType {
|
||||
CallReplaces = "m.call.replaces",
|
||||
CallAssertedIdentity = "m.call.asserted_identity",
|
||||
CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity",
|
||||
CallEncryptionKeysPrefix = "io.element.call.encryption_keys",
|
||||
KeyVerificationRequest = "m.key.verification.request",
|
||||
KeyVerificationStart = "m.key.verification.start",
|
||||
KeyVerificationCancel = "m.key.verification.cancel",
|
||||
@@ -136,49 +78,27 @@ export enum EventType {
|
||||
FullyRead = "m.fully_read",
|
||||
Tag = "m.tag",
|
||||
SpaceOrder = "org.matrix.msc3230.space_order", // MSC3230
|
||||
MarkedUnread = "m.marked_unread",
|
||||
|
||||
// User account_data events
|
||||
PushRules = "m.push_rules",
|
||||
Direct = "m.direct",
|
||||
IgnoredUserList = "m.ignored_user_list",
|
||||
InvitePermissionConfig = "m.invite_permission_config", // MSC4380
|
||||
|
||||
// to_device events
|
||||
RoomKey = "m.room_key",
|
||||
RoomKeyRequest = "m.room_key_request",
|
||||
ForwardedRoomKey = "m.forwarded_room_key",
|
||||
Dummy = "m.dummy",
|
||||
SecretRequest = "m.secret.request",
|
||||
SecretSend = "m.secret.send",
|
||||
|
||||
// Group call events
|
||||
GroupCallPrefix = "org.matrix.msc3401.call",
|
||||
GroupCallMemberPrefix = "org.matrix.msc3401.call.member",
|
||||
|
||||
// MatrixRTC events
|
||||
RTCSlot = "org.matrix.msc4143.rtc.slot",
|
||||
RTCMembership = "org.matrix.msc4143.rtc.member",
|
||||
CallNotify = "org.matrix.msc4075.call.notify",
|
||||
RTCNotification = "org.matrix.msc4075.rtc.notification",
|
||||
RTCDecline = "org.matrix.msc4310.rtc.decline",
|
||||
|
||||
// Policy servers
|
||||
RoomPolicy = "org.matrix.msc4284.policy",
|
||||
|
||||
// Retention
|
||||
RetentionPolicy = "m.room.retention",
|
||||
RetentionPolicyUnstable = "org.matrix.msc1763.retention",
|
||||
}
|
||||
|
||||
export enum RelationType {
|
||||
Annotation = "m.annotation",
|
||||
Replace = "m.replace",
|
||||
Reference = "m.reference",
|
||||
|
||||
// Don't use this yet: it's only the stable version. The code still assumes we support the unstable prefix and,
|
||||
// moreover, our tests currently use the unstable prefix. Use THREAD_RELATION_TYPE.name.
|
||||
// Once we support *only* the stable prefix, THREAD_RELATION_TYPE can die and we can switch to this.
|
||||
Thread = "m.thread",
|
||||
}
|
||||
|
||||
@@ -322,152 +242,17 @@ export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
|
||||
*/
|
||||
export const UNSIGNED_THREAD_ID_FIELD = new UnstableValue("thread_id", "org.matrix.msc4023.thread_id");
|
||||
|
||||
/**
|
||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/4115
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const UNSIGNED_MEMBERSHIP_FIELD = new NamespacedValue("membership", "io.element.msc4115.membership");
|
||||
|
||||
/**
|
||||
* Mapped type from event type to content type for all specified non-state room events.
|
||||
*/
|
||||
export interface TimelineEvents {
|
||||
[EventType.RoomMessage]: RoomMessageEventContent;
|
||||
[EventType.Sticker]: StickerEventContent;
|
||||
[EventType.Reaction]: ReactionEventContent;
|
||||
[EventType.CallReplaces]: MCallReplacesEvent;
|
||||
[EventType.CallAnswer]: MCallAnswer;
|
||||
[EventType.CallSelectAnswer]: MCallSelectAnswer;
|
||||
[EventType.CallNegotiate]: Omit<MCallInviteNegotiate, "offer">;
|
||||
[EventType.CallInvite]: MCallInviteNegotiate;
|
||||
[EventType.CallCandidates]: MCallCandidates;
|
||||
[EventType.CallHangup]: MCallHangupReject;
|
||||
[EventType.CallReject]: MCallHangupReject;
|
||||
[EventType.CallSDPStreamMetadataChangedPrefix]: MCallBase &
|
||||
EitherAnd<
|
||||
{ sdp_stream_metadata: SDPStreamMetadata },
|
||||
{ "org.matrix.msc3077.sdp_stream_metadata": SDPStreamMetadata }
|
||||
>;
|
||||
[EventType.CallSDPStreamMetadataChanged]: MCallBase &
|
||||
EitherAnd<
|
||||
{ sdp_stream_metadata: SDPStreamMetadata },
|
||||
{ "org.matrix.msc3077.sdp_stream_metadata": SDPStreamMetadata }
|
||||
>;
|
||||
[EventType.CallEncryptionKeysPrefix]: EncryptionKeysEventContent;
|
||||
[EventType.CallNotify]: ICallNotifyContent;
|
||||
[EventType.RTCNotification]: IRTCNotificationContent;
|
||||
[EventType.RTCDecline]: IRTCDeclineContent;
|
||||
[M_BEACON.name]: MBeaconEventContent;
|
||||
[M_POLL_START.name]: PollStartEventContent;
|
||||
[M_POLL_END.name]: PollEndEventContent;
|
||||
[EventType.RTCMembership]: RtcMembershipData | { msc4354_sticky_key: string }; // An object containing just the sticky key is empty.
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped type from event type to content type for all specified room state events.
|
||||
*/
|
||||
export interface StateEvents {
|
||||
[EventType.RoomCanonicalAlias]: RoomCanonicalAliasEventContent;
|
||||
[EventType.RoomCreate]: RoomCreateEventContent;
|
||||
[EventType.RoomJoinRules]: RoomJoinRulesEventContent;
|
||||
[EventType.RoomMember]: RoomMemberEventContent;
|
||||
// XXX: Spec says this event has 3 required fields but kicking such an invitation requires sending `{}`
|
||||
[EventType.RoomThirdPartyInvite]: RoomThirdPartyInviteEventContent | EmptyObject;
|
||||
[EventType.RoomPowerLevels]: RoomPowerLevelsEventContent;
|
||||
[EventType.RoomName]: RoomNameEventContent;
|
||||
[EventType.RoomTopic]: RoomTopicEventContent;
|
||||
[EventType.RoomAvatar]: RoomAvatarEventContent;
|
||||
[EventType.RoomPinnedEvents]: RoomPinnedEventsEventContent;
|
||||
[EventType.RoomEncryption]: RoomEncryptionEventContent;
|
||||
[EventType.RoomHistoryVisibility]: RoomHistoryVisibilityEventContent;
|
||||
[EventType.RoomGuestAccess]: RoomGuestAccessEventContent;
|
||||
[EventType.RoomServerAcl]: RoomServerAclEventContent;
|
||||
[EventType.RoomTombstone]: RoomTombstoneEventContent;
|
||||
[EventType.SpaceChild]: SpaceChildEventContent;
|
||||
[EventType.SpaceParent]: SpaceParentEventContent;
|
||||
|
||||
[EventType.PolicyRuleUser]: PolicyRuleEventContent | EmptyObject;
|
||||
[EventType.PolicyRuleRoom]: PolicyRuleEventContent | EmptyObject;
|
||||
[EventType.PolicyRuleServer]: PolicyRuleEventContent | EmptyObject;
|
||||
|
||||
// MSC4284: Policy servers
|
||||
[EventType.RoomPolicy]: RoomPolicyContent | EmptyObject;
|
||||
|
||||
// MSC3401
|
||||
[EventType.GroupCallPrefix]: IGroupCallRoomState;
|
||||
[EventType.GroupCallMemberPrefix]: IGroupCallRoomMemberState | SessionMembershipData | EmptyObject;
|
||||
[EventType.RTCMembership]: RtcMembershipData | EmptyObject;
|
||||
[EventType.RTCSlot]: RtcSlotEventContent | EmptyObject;
|
||||
// MSC3089
|
||||
[UNSTABLE_MSC3089_BRANCH.name]: MSC3089EventContent;
|
||||
|
||||
// MSC3672
|
||||
[M_BEACON_INFO.name]: MBeaconInfoEventContent;
|
||||
|
||||
// MSC1763
|
||||
[ROOM_RETENTION_TYPE.name]: RoomRetentionContent | EmptyObject;
|
||||
[ROOM_RETENTION_TYPE.altName]: RoomRetentionContent | EmptyObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped type from event type to content type for all specified room-specific account_data events.
|
||||
*/
|
||||
export interface RoomAccountDataEvents extends SecretStorageAccountDataEvents {
|
||||
[EventType.FullyRead]: { event_id: string };
|
||||
[EventType.Tag]: { tags: { [name: string]: { order?: number } } };
|
||||
[EventType.SpaceOrder]: { order: string };
|
||||
[EventType.MarkedUnread]: { unread: boolean };
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped type from event type to content type for all specified global account_data events.
|
||||
*/
|
||||
export interface AccountDataEvents extends SecretStorageAccountDataEvents {
|
||||
[EventType.PushRules]: IPushRules;
|
||||
[EventType.Direct]: { [userId: string]: string[] };
|
||||
[EventType.IgnoredUserList]: { ignored_users: { [userId: string]: EmptyObject } };
|
||||
"m.secret_storage.default_key": { key: string };
|
||||
|
||||
// MSC4287: Sharing key backup preference between clients - used to mark that the user opted out of key storage
|
||||
"m.key_backup": { enabled: boolean };
|
||||
// MSC4287 unstable prefix (note the boolean property has the opposite sense)
|
||||
"m.org.matrix.custom.backup_disabled": { disabled: boolean };
|
||||
|
||||
"m.identity_server": { base_url: string | null };
|
||||
[key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings;
|
||||
[key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription;
|
||||
|
||||
// Invites-ignorer events
|
||||
[POLICIES_ACCOUNT_EVENT_TYPE.name]: { [key: string]: any };
|
||||
[POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any };
|
||||
|
||||
[EventType.InvitePermissionConfig]: { default_action?: string };
|
||||
|
||||
// List of recently used reaction emojis
|
||||
// https://spec.matrix.org/v1.18/client-server-api/#mrecent_emoji
|
||||
"m.recent_emoji": {
|
||||
recent_emoji: Array<{
|
||||
emoji: string;
|
||||
total: number;
|
||||
}>;
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
key: {
|
||||
alg: string;
|
||||
key_ops: string[]; // eslint-disable-line camelcase
|
||||
kty: string;
|
||||
k: string;
|
||||
ext: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of AccountDataEvents, excluding events specified in https://spec.matrix.org/v1.17/client-server-api/#server-behaviour-12
|
||||
*/
|
||||
export type WritableAccountDataEvents = Exclude<AccountDataEvents, "m.fully_read" | "m.push_rules">;
|
||||
|
||||
/**
|
||||
* Mapped type from event type to content type for all specified global events encrypted by secret storage.
|
||||
*
|
||||
* See https://spec.matrix.org/v1.13/client-server-api/#msecret_storagev1aes-hmac-sha2-1
|
||||
*/
|
||||
export interface SecretStorageAccountDataEvents {
|
||||
"m.megolm_backup.v1": SecretInfo;
|
||||
"m.cross_signing.master": SecretInfo;
|
||||
"m.cross_signing.self_signing": SecretInfo;
|
||||
"m.cross_signing.user_signing": SecretInfo;
|
||||
"org.matrix.msc3814": SecretInfo;
|
||||
iv: string;
|
||||
hashes: { [alg: string]: string };
|
||||
v: string;
|
||||
}
|
||||
|
||||
119
node_modules/matrix-js-sdk/src/@types/events.ts
generated
vendored
119
node_modules/matrix-js-sdk/src/@types/events.ts
generated
vendored
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 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 { type MsgType, type RelationType } from "./event.ts";
|
||||
import { type FileInfo, type ImageInfo, type MediaEventContent } from "./media.ts";
|
||||
import { type XOR } from "./common.ts";
|
||||
|
||||
interface BaseTimelineEvent {
|
||||
"body": string;
|
||||
"m.mentions"?: {
|
||||
user_ids?: string[];
|
||||
room?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface ReplyEvent {
|
||||
"m.relates_to"?: {
|
||||
"m.in_reply_to"?: {
|
||||
event_id: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface NoRelationEvent {
|
||||
"m.new_content"?: never;
|
||||
"m.relates_to"?: never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial content format of timeline events with rel_type `m.replace`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#event-replacements
|
||||
*/
|
||||
export interface ReplacementEvent<T> {
|
||||
"m.new_content": T;
|
||||
"m.relates_to": {
|
||||
event_id: string;
|
||||
rel_type: RelationType.Replace;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial content format of timeline events with rel_type other than `m.replace`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#forming-relationships-between-events
|
||||
*/
|
||||
export interface RelationEvent {
|
||||
"m.new_content"?: never;
|
||||
"m.relates_to": {
|
||||
event_id: string;
|
||||
rel_type: Exclude<RelationType, RelationType.Replace>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of timeline events with type `m.room.message` and `msgtype` `m.text`, `m.emote`, or `m.notice`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mroommessage
|
||||
*/
|
||||
export interface RoomMessageTextEventContent extends BaseTimelineEvent {
|
||||
msgtype: MsgType.Text | MsgType.Emote | MsgType.Notice;
|
||||
format?: "org.matrix.custom.html";
|
||||
formatted_body?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of timeline events with type `m.room.message` and `msgtype` `m.location`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mlocation
|
||||
*/
|
||||
export interface RoomMessageLocationEventContent extends BaseTimelineEvent {
|
||||
body: string;
|
||||
geo_uri: string;
|
||||
info: Pick<FileInfo, "thumbnail_info" | "thumbnail_file" | "thumbnail_url">;
|
||||
msgtype: MsgType.Location;
|
||||
}
|
||||
|
||||
type MessageEventContent = RoomMessageTextEventContent | RoomMessageLocationEventContent | MediaEventContent;
|
||||
|
||||
export type RoomMessageEventContent = BaseTimelineEvent &
|
||||
XOR<XOR<ReplacementEvent<MessageEventContent>, RelationEvent>, XOR<ReplyEvent, NoRelationEvent>> &
|
||||
MessageEventContent;
|
||||
|
||||
/**
|
||||
* Content format of timeline events with type `m.sticker`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#msticker
|
||||
*/
|
||||
export interface StickerEventContent extends BaseTimelineEvent {
|
||||
body: string;
|
||||
info: ImageInfo;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of timeline events with type `m.reaction`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.9/client-server-api/#mreaction
|
||||
*/
|
||||
export interface ReactionEventContent {
|
||||
"m.relates_to": {
|
||||
event_id: string;
|
||||
key: string;
|
||||
rel_type: RelationType.Annotation;
|
||||
};
|
||||
}
|
||||
29
node_modules/matrix-js-sdk/src/@types/extensible_events.ts
generated
vendored
29
node_modules/matrix-js-sdk/src/@types/extensible_events.ts
generated
vendored
@@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type EitherAnd, NamespacedValue, UnstableValue } from "matrix-events-sdk";
|
||||
import { EitherAnd, NamespacedValue, Optional, UnstableValue } from "matrix-events-sdk";
|
||||
|
||||
import { isProvided } from "../extensible_events_v1/utilities.ts";
|
||||
import { isProvided } from "../extensible_events_v1/utilities";
|
||||
|
||||
// Types and utilities for MSC1767: Extensible events (version 1) in Matrix
|
||||
|
||||
/**
|
||||
* Represents the stable and unstable values of a given namespace.
|
||||
*/
|
||||
export type TSNamespace<N> =
|
||||
N extends NamespacedValue<infer S, infer U> ? TSNamespaceValue<S> | TSNamespaceValue<U> : never;
|
||||
export type TSNamespace<N> = N extends NamespacedValue<infer S, infer U>
|
||||
? TSNamespaceValue<S> | TSNamespaceValue<U>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Represents a namespaced value, if the value is a string. Used to extract provided types
|
||||
@@ -97,12 +98,12 @@ export type AnyRelation = TSNamespace<typeof REFERENCE_RELATION> | string;
|
||||
/**
|
||||
* An m.relates_to relationship
|
||||
*/
|
||||
export type RelatesToRelationship<R = never> = {
|
||||
export type RelatesToRelationship<R = never, C = never> = {
|
||||
"m.relates_to": {
|
||||
// See https://github.com/microsoft/TypeScript/issues/23182#issuecomment-379091887 for array syntax
|
||||
rel_type: [R] extends [never] ? AnyRelation : TSNamespace<R>;
|
||||
event_id: string;
|
||||
};
|
||||
} & DefaultNever<C, {}>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -125,21 +126,25 @@ export type ExtensibleEventType = NamespacedValue<string, string> | string;
|
||||
* @param expected - The expected event type.
|
||||
* @returns True if the given type matches the expected type.
|
||||
*/
|
||||
export function isEventTypeSame(given: ExtensibleEventType | null, expected: ExtensibleEventType | null): boolean {
|
||||
export function isEventTypeSame(
|
||||
given: Optional<ExtensibleEventType>,
|
||||
expected: Optional<ExtensibleEventType>,
|
||||
): boolean {
|
||||
if (typeof given === "string") {
|
||||
if (typeof expected === "string") {
|
||||
return expected === given;
|
||||
} else {
|
||||
return expected!.matches(given);
|
||||
return (expected as NamespacedValue<string, string>).matches(given as string);
|
||||
}
|
||||
} else {
|
||||
if (typeof expected === "string") {
|
||||
return given!.matches(expected);
|
||||
return (given as NamespacedValue<string, string>).matches(expected as string);
|
||||
} else {
|
||||
const expectedNs = expected!;
|
||||
const givenNs = given!;
|
||||
const expectedNs = expected as NamespacedValue<string, string>;
|
||||
const givenNs = given as NamespacedValue<string, string>;
|
||||
return (
|
||||
expectedNs.matches(givenNs.name) || (isProvided(givenNs.altName) && expectedNs.matches(givenNs.altName))
|
||||
expectedNs.matches(givenNs.name) ||
|
||||
(isProvided(givenNs.altName) && expectedNs.matches(givenNs.altName!))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
92
node_modules/matrix-js-sdk/src/@types/global.d.ts
generated
vendored
92
node_modules/matrix-js-sdk/src/@types/global.d.ts
generated
vendored
@@ -14,54 +14,102 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// this is needed to tell TS about global.Olm
|
||||
import "@matrix-org/olm";
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
// use `number` as the return type in all cases for globalThis.set{Interval,Timeout},
|
||||
// use `number` as the return type in all cases for global.set{Interval,Timeout},
|
||||
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
|
||||
// The overload for clear{Interval,Timeout} is resolved as expected.
|
||||
// We use `ReturnType<typeof setTimeout>` in the code to be agnostic of if this definition gets loaded.
|
||||
function setInterval(handler: TimerHandler, timeout: number, ...args: any[]): number;
|
||||
function setTimeout(handler: TimerHandler, timeout: number, ...args: any[]): number;
|
||||
function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||
function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
localStorage: Storage;
|
||||
// marker variable used to detect both the browser & node entrypoints being used at once
|
||||
__js_sdk_entrypoint: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome-specific getUserMedia constraints
|
||||
interface MediaTrackConstraints {
|
||||
mandatory?: {
|
||||
chromeMediaSource: string;
|
||||
chromeMediaSourceId: string;
|
||||
};
|
||||
interface Window {
|
||||
webkitAudioContext: typeof AudioContext;
|
||||
}
|
||||
|
||||
interface Crypto {
|
||||
webkitSubtle?: Window["crypto"]["subtle"];
|
||||
}
|
||||
|
||||
interface MediaDevices {
|
||||
// This is experimental and types don't know about it yet
|
||||
// https://github.com/microsoft/TypeScript/issues/33232
|
||||
getDisplayMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
|
||||
getUserMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
|
||||
}
|
||||
|
||||
interface DesktopCapturerConstraints {
|
||||
audio:
|
||||
| boolean
|
||||
| {
|
||||
mandatory: {
|
||||
chromeMediaSource: string;
|
||||
chromeMediaSourceId: string;
|
||||
};
|
||||
};
|
||||
video:
|
||||
| boolean
|
||||
| {
|
||||
mandatory: {
|
||||
chromeMediaSource: string;
|
||||
chromeMediaSourceId: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface DummyInterfaceWeShouldntBeUsingThis {}
|
||||
|
||||
interface Navigator {
|
||||
// We check for the webkit-prefixed getUserMedia to detect if we're
|
||||
// on webkit: we should check if we still need to do this
|
||||
webkitGetUserMedia?: unknown;
|
||||
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
|
||||
}
|
||||
|
||||
export interface Uint8ArrayToBase64Options {
|
||||
alphabet?: "base64" | "base64url";
|
||||
omitPadding?: boolean;
|
||||
export interface ISettledFulfilled<T> {
|
||||
status: "fulfilled";
|
||||
value: T;
|
||||
}
|
||||
export interface ISettledRejected {
|
||||
status: "rejected";
|
||||
reason: any;
|
||||
}
|
||||
|
||||
interface Uint8Array {
|
||||
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
|
||||
toBase64?(options?: Uint8ArrayToBase64Options): string;
|
||||
interface PromiseConstructor {
|
||||
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
|
||||
}
|
||||
|
||||
export interface Uint8ArrayFromBase64Options {
|
||||
alphabet?: "base64"; // Our fallback code only handles base64.
|
||||
lastChunkHandling?: "loose"; // Our fallback code doesn't support other handling at this time.
|
||||
interface RTCRtpTransceiver {
|
||||
// This has been removed from TS
|
||||
// (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029),
|
||||
// but we still need this for MatrixCall::getRidOfRTXCodecs()
|
||||
setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
|
||||
}
|
||||
|
||||
interface Uint8ArrayConstructor {
|
||||
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64
|
||||
fromBase64?(base64: string, options?: Uint8ArrayFromBase64Options): Uint8Array<ArrayBuffer>;
|
||||
interface RequestInit {
|
||||
/**
|
||||
* Specifies the priority of the fetch request relative to other requests of the same type.
|
||||
* Must be one of the following strings:
|
||||
* high: A high priority fetch request relative to other requests of the same type.
|
||||
* low: A low priority fetch request relative to other requests of the same type.
|
||||
* auto: Automatically determine the priority of the fetch request relative to other requests of the same type (default).
|
||||
*
|
||||
* @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attribute
|
||||
* @see https://github.com/microsoft/TypeScript/issues/54472
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#browser_compatibility
|
||||
* Not yet supported in Safari or Firefox
|
||||
*/
|
||||
priority?: "high" | "low" | "auto";
|
||||
}
|
||||
}
|
||||
|
||||
16
node_modules/matrix-js-sdk/src/@types/json.ts
generated
vendored
16
node_modules/matrix-js-sdk/src/@types/json.ts
generated
vendored
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// Types for JSON and JSON objects, copied from element-web (left in both places as I don't think we
|
||||
// want this as a part of js-sdk's interface and I don't think it's worth it being its own package)
|
||||
|
||||
export type JsonValue = null | string | number | boolean;
|
||||
export type JsonArray = Array<JsonValue | JsonObject | JsonArray>;
|
||||
export interface JsonObject {
|
||||
[key: string]: JsonObject | JsonArray | JsonValue;
|
||||
}
|
||||
export type Json = JsonArray | JsonObject;
|
||||
6
node_modules/matrix-js-sdk/src/@types/location.ts
generated
vendored
6
node_modules/matrix-js-sdk/src/@types/location.ts
generated
vendored
@@ -15,10 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// Types for MSC3488 - m.location: Extending events with location data
|
||||
import { type EitherAnd } from "matrix-events-sdk";
|
||||
import { EitherAnd } from "matrix-events-sdk";
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue.ts";
|
||||
import { type M_TEXT } from "./extensible_events.ts";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { M_TEXT } from "./extensible_events";
|
||||
|
||||
export enum LocationAssetType {
|
||||
Self = "m.self",
|
||||
|
||||
38
node_modules/matrix-js-sdk/src/@types/matrix-sdk-crypto-wasm.d.ts
generated
vendored
38
node_modules/matrix-js-sdk/src/@types/matrix-sdk-crypto-wasm.d.ts
generated
vendored
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 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 type * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
declare module "@matrix-org/matrix-sdk-crypto-wasm" {
|
||||
interface SecretsBundle {
|
||||
to_json(): Promise<{
|
||||
cross_signing: {
|
||||
master_key: string;
|
||||
self_signing_key: string;
|
||||
user_signing_key: string;
|
||||
};
|
||||
backup?: {
|
||||
algorithm: string;
|
||||
key: string;
|
||||
backup_version: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
interface Device {
|
||||
requestVerification(methods?: any[]): [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest];
|
||||
}
|
||||
}
|
||||
245
node_modules/matrix-js-sdk/src/@types/media.ts
generated
vendored
245
node_modules/matrix-js-sdk/src/@types/media.ts
generated
vendored
@@ -1,245 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 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 { type MsgType } from "../@types/event.ts";
|
||||
|
||||
/**
|
||||
* Information on encrypted media attachments.
|
||||
*
|
||||
* Used within `m.room.message` events that reference files, such as `m.file` and `m.image`.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#extensions-to-mroommessage-msgtypes
|
||||
*/
|
||||
export interface EncryptedFile {
|
||||
/**
|
||||
* The URL to the file.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* A JSON Web Key object.
|
||||
*/
|
||||
key: {
|
||||
alg: string;
|
||||
key_ops: string[];
|
||||
kty: string;
|
||||
k: string;
|
||||
ext: boolean;
|
||||
};
|
||||
/**
|
||||
* The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64.
|
||||
*/
|
||||
iv: string;
|
||||
/**
|
||||
* A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64.
|
||||
* Clients should support the SHA-256 hash, which uses the key `sha256`.
|
||||
*/
|
||||
hashes: { [alg: string]: string };
|
||||
/**
|
||||
* Version of the encrypted attachment's protocol. Must be `v2`.
|
||||
*/
|
||||
v: string;
|
||||
}
|
||||
|
||||
interface ThumbnailInfo {
|
||||
/**
|
||||
* The mimetype of the image, e.g. image/jpeg.
|
||||
*/
|
||||
mimetype?: string;
|
||||
/**
|
||||
* The intended display width of the image in pixels.
|
||||
* This may differ from the intrinsic dimensions of the image file.
|
||||
*/
|
||||
w?: number;
|
||||
/**
|
||||
* The intended display height of the image in pixels.
|
||||
* This may differ from the intrinsic dimensions of the image file.
|
||||
*/
|
||||
h?: number;
|
||||
/**
|
||||
* Size of the image in bytes.
|
||||
*/
|
||||
size?: number;
|
||||
}
|
||||
|
||||
interface BaseInfo {
|
||||
mimetype?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information on media attachments of msgtype `m.file`
|
||||
*
|
||||
* Used within `m.room.message` events that reference files.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mfile
|
||||
*/
|
||||
export interface FileInfo extends BaseInfo {
|
||||
/**
|
||||
* Information on the encrypted thumbnail file, as specified in End-to-end encryption.
|
||||
* Only present if the thumbnail is encrypted.
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#sending-encrypted-attachments
|
||||
*/
|
||||
thumbnail_file?: EncryptedFile;
|
||||
/**
|
||||
* Metadata about the image referred to in thumbnail_url.
|
||||
*/
|
||||
thumbnail_info?: ThumbnailInfo;
|
||||
/**
|
||||
* The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted.
|
||||
*/
|
||||
thumbnail_url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information on media attachments of msgtype `m.image`
|
||||
*
|
||||
* Used within `m.room.message` events that reference images.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mimage
|
||||
*/
|
||||
export interface ImageInfo extends FileInfo, ThumbnailInfo {}
|
||||
|
||||
/**
|
||||
* Information on media attachments of msgtype `m.audio`
|
||||
*
|
||||
* Used within `m.room.message` events that reference audio files.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#maudio
|
||||
*/
|
||||
export interface AudioInfo extends BaseInfo {
|
||||
/**
|
||||
* The duration of the audio in milliseconds.
|
||||
*/
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information on media attachments of msgtype `m.video`
|
||||
*
|
||||
* Used within `m.room.message` events that reference video files.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mvideo
|
||||
*/
|
||||
export interface VideoInfo extends AudioInfo, ImageInfo {
|
||||
/**
|
||||
* The duration of the video in milliseconds.
|
||||
*/
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type representing the `content.info` field of all specified media events.
|
||||
*/
|
||||
export type MediaEventInfo = FileInfo | ImageInfo | AudioInfo | VideoInfo;
|
||||
|
||||
interface BaseContent {
|
||||
/**
|
||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#sending-encrypted-attachments
|
||||
*/
|
||||
file?: EncryptedFile;
|
||||
/**
|
||||
* Required if the file is unencrypted. The URL (typically mxc:// URI) to the file.
|
||||
*/
|
||||
url?: string;
|
||||
/**
|
||||
* If filename is not set or the value of both properties are identical,
|
||||
* this is the filename of the original upload. Otherwise, this is a
|
||||
* caption for the file.
|
||||
*/
|
||||
body: string;
|
||||
/**
|
||||
* The original filename of the uploaded file.
|
||||
*/
|
||||
filename?: string;
|
||||
/**
|
||||
* The format used in the `formatted_body`.
|
||||
*/
|
||||
format?: "org.matrix.custom.html";
|
||||
/**
|
||||
* The formatted version of the `body`, when it acts as a caption. This is required if `format` is specified.
|
||||
*/
|
||||
formatted_body?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of media events with msgtype `m.file`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mfile
|
||||
*/
|
||||
export interface FileContent extends BaseContent {
|
||||
/**
|
||||
* Information about the file referred to in url.
|
||||
*/
|
||||
info?: FileInfo;
|
||||
/**
|
||||
* One of: [m.file].
|
||||
*/
|
||||
msgtype: MsgType.File;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of media events with msgtype `m.image`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mimage
|
||||
*/
|
||||
export interface ImageContent extends BaseContent {
|
||||
/**
|
||||
* Metadata about the image referred to in url.
|
||||
*/
|
||||
info?: ImageInfo;
|
||||
/**
|
||||
* One of: [m.image].
|
||||
*/
|
||||
msgtype: MsgType.Image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of media events with msgtype `m.audio`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#maudio
|
||||
*/
|
||||
export interface AudioContent extends BaseContent {
|
||||
/**
|
||||
* Metadata for the audio clip referred to in url.
|
||||
*/
|
||||
info?: AudioInfo;
|
||||
/**
|
||||
* One of: [m.audio].
|
||||
*/
|
||||
msgtype: MsgType.Audio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content format of media events with msgtype `m.video`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#mvideo
|
||||
*/
|
||||
export interface VideoContent extends BaseContent {
|
||||
/**
|
||||
* Metadata about the video clip referred to in url.
|
||||
*/
|
||||
info?: VideoInfo;
|
||||
/**
|
||||
* One of: [m.video].
|
||||
*/
|
||||
msgtype: MsgType.Video;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing media event contents for `m.room.message` events listed in the Matrix specification
|
||||
*/
|
||||
export type MediaEventContent = FileContent | ImageContent | AudioContent | VideoContent;
|
||||
57
node_modules/matrix-js-sdk/src/@types/membership.ts
generated
vendored
57
node_modules/matrix-js-sdk/src/@types/membership.ts
generated
vendored
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Well-known values (from the spec or MSCs) that are allowed in the
|
||||
* {@link Membership} type.
|
||||
*/
|
||||
export enum KnownMembership {
|
||||
/**
|
||||
* The user has been banned from the room, and is no longer allowed to join
|
||||
* it until they are un-banned from the room (by having their membership
|
||||
* state set to a value other than ban).
|
||||
*/
|
||||
Ban = "ban",
|
||||
/**
|
||||
* The user has been invited to join a room, but has not yet joined it.
|
||||
* They may not participate in the room until they join.
|
||||
* */
|
||||
Invite = "invite",
|
||||
/**
|
||||
* The user has joined the room (possibly after accepting an invite), and
|
||||
* may participate in it.
|
||||
*/
|
||||
Join = "join",
|
||||
/**
|
||||
* The user has knocked on the room, requesting permission to participate.
|
||||
* They may not participate in the room until they join.
|
||||
*/
|
||||
Knock = "knock",
|
||||
/**
|
||||
* The user was once joined to the room, but has since left (possibly by
|
||||
* choice, or possibly by being kicked).
|
||||
*/
|
||||
Leave = "leave",
|
||||
}
|
||||
|
||||
/**
|
||||
* The membership state for a user in a room [1]. A value from
|
||||
* {@link KnownMembership} should be used where available, but all string values
|
||||
* are allowed to provide flexibility for upcoming spec changes or proposals.
|
||||
*
|
||||
* [1] https://spec.matrix.org/latest/client-server-api/#mroommember
|
||||
*/
|
||||
export type Membership = KnownMembership | string;
|
||||
60
node_modules/matrix-js-sdk/src/@types/partials.ts
generated
vendored
60
node_modules/matrix-js-sdk/src/@types/partials.ts
generated
vendored
@@ -14,6 +14,20 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export interface IImageInfo {
|
||||
size?: number;
|
||||
mimetype?: string;
|
||||
thumbnail_info?: {
|
||||
// eslint-disable-line camelcase
|
||||
w?: number;
|
||||
h?: number;
|
||||
size?: number;
|
||||
mimetype?: string;
|
||||
};
|
||||
w?: number;
|
||||
h?: number;
|
||||
}
|
||||
|
||||
export enum Visibility {
|
||||
Public = "public",
|
||||
Private = "private",
|
||||
@@ -45,6 +59,14 @@ export enum RestrictedAllowType {
|
||||
RoomMembership = "m.room_membership",
|
||||
}
|
||||
|
||||
export interface IJoinRuleEventContent {
|
||||
join_rule: JoinRule; // eslint-disable-line camelcase
|
||||
allow?: {
|
||||
type: RestrictedAllowType;
|
||||
room_id: string; // eslint-disable-line camelcase
|
||||
}[];
|
||||
}
|
||||
|
||||
export enum GuestAccess {
|
||||
CanJoin = "can_join",
|
||||
Forbidden = "forbidden",
|
||||
@@ -60,42 +82,8 @@ export enum HistoryVisibility {
|
||||
export interface IUsageLimit {
|
||||
// "hs_disabled" is NOT a specced string, but is used in Synapse
|
||||
// This is tracked over at https://github.com/matrix-org/synapse/issues/9237
|
||||
// eslint-disable-next-line camelcase
|
||||
limit_type: "monthly_active_user" | "hs_disabled" | string;
|
||||
// eslint-disable-next-line camelcase
|
||||
admin_contact?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A policy name & url in a specific internationalisation
|
||||
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms_response-200_internationalised-policy
|
||||
*/
|
||||
export interface InternationalisedPolicy {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A versioned policy with internationalised variants
|
||||
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms_response-200_policy-object
|
||||
*/
|
||||
export interface Policy {
|
||||
/**
|
||||
* The version for the policy.
|
||||
* There are no requirements on what this might be and could be “alpha”, semantically versioned, or arbitrary.
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* The policy information for the specified language.
|
||||
* @remarks the type has to include a union with string due to limitations in the type system.
|
||||
*/
|
||||
[lang: string]: InternationalisedPolicy | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from the Terms API for Identity servers
|
||||
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms
|
||||
*/
|
||||
export interface Terms {
|
||||
policies: {
|
||||
[policyName: string]: Policy;
|
||||
};
|
||||
}
|
||||
|
||||
15
node_modules/matrix-js-sdk/src/@types/polls.ts
generated
vendored
15
node_modules/matrix-js-sdk/src/@types/polls.ts
generated
vendored
@@ -14,15 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type EitherAnd, UnstableValue } from "matrix-events-sdk";
|
||||
import { EitherAnd, UnstableValue } from "matrix-events-sdk";
|
||||
|
||||
import {
|
||||
type ExtensibleAnyMessageEventContent,
|
||||
type REFERENCE_RELATION,
|
||||
type RelatesToRelationship,
|
||||
type TSNamespace,
|
||||
} from "./extensible_events.ts";
|
||||
import { type EmptyObject } from "./common.ts";
|
||||
ExtensibleAnyMessageEventContent,
|
||||
REFERENCE_RELATION,
|
||||
RelatesToRelationship,
|
||||
TSNamespace,
|
||||
} from "./extensible_events";
|
||||
|
||||
/**
|
||||
* Identifier for a disclosed poll.
|
||||
@@ -110,7 +109,7 @@ export const M_POLL_END = new UnstableValue("m.poll.end", "org.matrix.msc3381.po
|
||||
/**
|
||||
* The event definition for an m.poll.end event (in content)
|
||||
*/
|
||||
export type PollEndEvent = EitherAnd<{ [M_POLL_END.name]: EmptyObject }, { [M_POLL_END.altName]: EmptyObject }>;
|
||||
export type PollEndEvent = EitherAnd<{ [M_POLL_END.name]: {} }, { [M_POLL_END.altName]: {} }>;
|
||||
|
||||
/**
|
||||
* The content for an m.poll.end event
|
||||
|
||||
26
node_modules/matrix-js-sdk/src/@types/registration.ts
generated
vendored
26
node_modules/matrix-js-sdk/src/@types/registration.ts
generated
vendored
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type AuthDict } from "../interactive-auth.ts";
|
||||
import { AuthDict } from "../interactive-auth";
|
||||
|
||||
/**
|
||||
* The request body of a call to `POST /_matrix/client/v3/register`.
|
||||
@@ -52,13 +52,21 @@ export interface RegisterRequest {
|
||||
*/
|
||||
initial_device_display_name?: string;
|
||||
/**
|
||||
* Guest users can also upgrade their account by going through the ordinary register flow,
|
||||
* but specifying the additional POST parameter guest_access_token containing the guest’s access token.
|
||||
* They are also required to specify the username parameter to the value of the local part of their username,
|
||||
* which is otherwise optional.
|
||||
* @see https://spec.matrix.org/v1.10/client-server-api/#guest-access
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
guest_access_token?: string;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
x_show_msisdn?: boolean;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
bind_msisdn?: boolean;
|
||||
/**
|
||||
* @deprecated missing in the spec
|
||||
*/
|
||||
bind_email?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,4 +107,10 @@ export interface RegisterResponse {
|
||||
* Omitted if the inhibit_login option is true.
|
||||
*/
|
||||
refresh_token?: string;
|
||||
/**
|
||||
* The server_name of the homeserver on which the account has been registered.
|
||||
*
|
||||
* @deprecated Clients should extract the server_name from user_id (by splitting at the first colon) if they require it.
|
||||
*/
|
||||
home_server?: string;
|
||||
}
|
||||
|
||||
156
node_modules/matrix-js-sdk/src/@types/requests.ts
generated
vendored
156
node_modules/matrix-js-sdk/src/@types/requests.ts
generated
vendored
@@ -14,17 +14,26 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type IContent, type IEvent } from "../models/event.ts";
|
||||
import { type Preset, type Visibility } from "./partials.ts";
|
||||
import { type IEventWithRoomId, type SearchKey } from "./search.ts";
|
||||
import { type IRoomEventFilter } from "../filter.ts";
|
||||
import { type Direction } from "../models/event-timeline.ts";
|
||||
import { type PushRuleAction } from "./PushRules.ts";
|
||||
import { type MatrixError } from "../matrix.ts";
|
||||
import { type IRoomEvent } from "../sync-accumulator.ts";
|
||||
import { type EventType, type RelationType, type RoomType } from "./event.ts";
|
||||
import { IContent, IEvent } from "../models/event";
|
||||
import { Preset, Visibility } from "./partials";
|
||||
import { IEventWithRoomId, SearchKey } from "./search";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
import { Direction } from "../models/event-timeline";
|
||||
import { PushRuleAction } from "./PushRules";
|
||||
import { IRoomEvent } from "../sync-accumulator";
|
||||
import { EventType, RelationType, RoomType } from "./event";
|
||||
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export interface IJoinRoomOpts {
|
||||
/**
|
||||
* True to do a room initial sync on the resulting
|
||||
* room. If false, the <strong>returned Room object will have no current state.
|
||||
* </strong> Default: true.
|
||||
*/
|
||||
syncRoom?: boolean;
|
||||
|
||||
/**
|
||||
* If the caller has a keypair 3pid invite, the signing URL is passed in this parameter.
|
||||
*/
|
||||
@@ -32,46 +41,8 @@ export interface IJoinRoomOpts {
|
||||
|
||||
/**
|
||||
* The server names to try and join through in addition to those that are automatically chosen.
|
||||
* Only the first 3 are actually used in the request, to avoid HTTP 414 Request-URI Too Long responses.
|
||||
*/
|
||||
viaServers?: string[];
|
||||
|
||||
/**
|
||||
* Previously, configured whether to accept encrypted history shared by the inviter. This is now always enabled,
|
||||
* and the setting is only retained to avoid a breaking change to the API. It has no effect.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
acceptSharedHistory?: boolean;
|
||||
}
|
||||
|
||||
/** Options object for {@link MatrixClient.invite}. */
|
||||
export interface InviteOpts {
|
||||
/**
|
||||
* The reason for the invite.
|
||||
*/
|
||||
reason?: string;
|
||||
|
||||
/**
|
||||
* Previously, configured whether to send encrypted history if the visibility settings allow it.
|
||||
* This is now always enabled, and the setting is only retained to avoid a breaking change to the API. It has no effect.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
shareEncryptedHistory?: boolean;
|
||||
}
|
||||
|
||||
export interface KnockRoomOpts {
|
||||
/**
|
||||
* The reason for the knock.
|
||||
*/
|
||||
reason?: string;
|
||||
|
||||
/**
|
||||
* The server names to try and knock through in addition to those that are automatically chosen.
|
||||
* Only the first 3 are actually used in the request, to avoid HTTP 414 Request-URI Too Long responses.
|
||||
*/
|
||||
viaServers?: string | string[];
|
||||
}
|
||||
|
||||
export interface IRedactOpts {
|
||||
@@ -93,65 +64,6 @@ export interface ISendEventResponse {
|
||||
event_id: string;
|
||||
}
|
||||
|
||||
export type SendDelayedEventRequestOpts = { parent_delay_id: string } | { delay: number; parent_delay_id?: string };
|
||||
|
||||
export function isSendDelayedEventRequestOpts(opts: object): opts is SendDelayedEventRequestOpts {
|
||||
if ("parent_delay_id" in opts && typeof opts.parent_delay_id !== "string") {
|
||||
// Invalid type, reject
|
||||
return false;
|
||||
}
|
||||
if ("delay" in opts && typeof opts.delay !== "number") {
|
||||
// Invalid type, reject.
|
||||
return true;
|
||||
}
|
||||
// At least one of these fields must be specified.
|
||||
return "delay" in opts || "parent_delay_id" in opts;
|
||||
}
|
||||
export type SendDelayedEventResponse = {
|
||||
delay_id: string;
|
||||
};
|
||||
|
||||
export enum UpdateDelayedEventAction {
|
||||
Cancel = "cancel",
|
||||
Restart = "restart",
|
||||
Send = "send",
|
||||
}
|
||||
|
||||
export type UpdateDelayedEventRequestOpts = SendDelayedEventResponse & {
|
||||
action: UpdateDelayedEventAction;
|
||||
};
|
||||
|
||||
type DelayedPartialTimelineEvent = {
|
||||
room_id: string;
|
||||
type: string;
|
||||
content: IContent;
|
||||
};
|
||||
|
||||
type DelayedPartialStateEvent = DelayedPartialTimelineEvent & {
|
||||
state_key: string;
|
||||
};
|
||||
|
||||
type DelayedPartialEvent = DelayedPartialTimelineEvent | DelayedPartialStateEvent;
|
||||
|
||||
export type DelayedEventInfoItem = DelayedPartialEvent &
|
||||
SendDelayedEventResponse &
|
||||
SendDelayedEventRequestOpts & {
|
||||
running_since: number;
|
||||
};
|
||||
|
||||
export type DelayedEventInfo = {
|
||||
scheduled?: DelayedEventInfoItem[];
|
||||
finalised?: {
|
||||
delayed_event: DelayedEventInfoItem;
|
||||
outcome: "send" | "cancel";
|
||||
reason: "error" | "action" | "delay";
|
||||
error?: MatrixError["data"];
|
||||
event_id?: string;
|
||||
origin_server_ts?: number;
|
||||
}[];
|
||||
next_batch?: string;
|
||||
};
|
||||
|
||||
export interface IPresenceOpts {
|
||||
// One of "online", "offline" or "unavailable"
|
||||
presence: "online" | "offline" | "unavailable";
|
||||
@@ -238,21 +150,11 @@ export interface ICreateRoomOpts {
|
||||
}
|
||||
|
||||
export interface IRoomDirectoryOptions {
|
||||
/**
|
||||
* The remote server to query for the room list.
|
||||
* Optional. If unspecified, get the local homeserver's public room list.
|
||||
*/
|
||||
server?: string;
|
||||
/**
|
||||
* Maximum number of entries to return
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* Token to paginate from
|
||||
*/
|
||||
since?: string;
|
||||
|
||||
/** Filter parameters */
|
||||
// Filter parameters
|
||||
filter?: {
|
||||
// String to search for
|
||||
generic_search_term?: string;
|
||||
@@ -279,6 +181,12 @@ export interface IBindThreePidBody {
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface IAddThreePidBody {
|
||||
client_secret: string;
|
||||
id_server: string;
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface IRelationsRequestOpts {
|
||||
from?: string;
|
||||
to?: string;
|
||||
@@ -294,12 +202,12 @@ export interface IRelationsResponse {
|
||||
}
|
||||
|
||||
export interface IContextResponse {
|
||||
end?: string;
|
||||
start?: string;
|
||||
state?: IEventWithRoomId[];
|
||||
events_before?: IEventWithRoomId[];
|
||||
events_after?: IEventWithRoomId[];
|
||||
event?: IEventWithRoomId;
|
||||
end: string;
|
||||
start: string;
|
||||
state: IEventWithRoomId[];
|
||||
events_before: IEventWithRoomId[];
|
||||
events_after: IEventWithRoomId[];
|
||||
event: IEventWithRoomId;
|
||||
}
|
||||
|
||||
export interface IEventsResponse {
|
||||
@@ -338,3 +246,5 @@ export interface IStatusResponse extends IPresenceOpts {
|
||||
currently_active?: boolean;
|
||||
last_active_ago?: number;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
16
node_modules/matrix-js-sdk/src/@types/retention.ts
generated
vendored
16
node_modules/matrix-js-sdk/src/@types/retention.ts
generated
vendored
@@ -1,16 +0,0 @@
|
||||
import { UnstableValue } from "../NamespacedValue.ts";
|
||||
|
||||
/**
|
||||
* Event type for a room policy event.
|
||||
* NOTE: While MSC1763 has not been merged, `m.room.retention` is unfortunately
|
||||
* already in use in production.
|
||||
*/
|
||||
export const ROOM_RETENTION_TYPE = new UnstableValue("m.room.retention", "org.matrix.msc1763.retention");
|
||||
|
||||
/**
|
||||
* The content of a `m.room.retention` state event.
|
||||
*/
|
||||
export interface RoomRetentionContent {
|
||||
max_lifetime?: number;
|
||||
min_lifetime?: number;
|
||||
}
|
||||
14
node_modules/matrix-js-sdk/src/@types/search.ts
generated
vendored
14
node_modules/matrix-js-sdk/src/@types/search.ts
generated
vendored
@@ -16,10 +16,11 @@ limitations under the License.
|
||||
|
||||
// Types relating to the /search API
|
||||
|
||||
import { type IRoomEvent, type IStateEvent } from "../sync-accumulator.ts";
|
||||
import { type IRoomEventFilter } from "../filter.ts";
|
||||
import { type SearchResult } from "../models/search-result.ts";
|
||||
import { IRoomEvent, IStateEvent } from "../sync-accumulator";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
import { SearchResult } from "../models/search-result";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IEventWithRoomId extends IRoomEvent {
|
||||
room_id: string;
|
||||
}
|
||||
@@ -53,9 +54,9 @@ enum GroupKey {
|
||||
}
|
||||
|
||||
export interface IResultRoomEvents {
|
||||
count?: number;
|
||||
highlights?: string[];
|
||||
results?: ISearchResult[];
|
||||
count: number;
|
||||
highlights: string[];
|
||||
results: ISearchResult[];
|
||||
state?: { [roomId: string]: IStateEventWithRoomId[] };
|
||||
groups?: {
|
||||
[groupKey in GroupKey]: {
|
||||
@@ -115,3 +116,4 @@ export interface ISearchResults {
|
||||
pendingRequest?: Promise<ISearchResults>;
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
8
node_modules/matrix-js-sdk/src/@types/spaces.ts
generated
vendored
8
node_modules/matrix-js-sdk/src/@types/spaces.ts
generated
vendored
@@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type IPublicRoomsChunkRoom } from "../client.ts";
|
||||
import { type RoomType } from "./event.ts";
|
||||
import { type IStrippedState } from "../sync-accumulator.ts";
|
||||
import { IPublicRoomsChunkRoom } from "../client";
|
||||
import { RoomType } from "./event";
|
||||
import { IStrippedState } from "../sync-accumulator";
|
||||
|
||||
// Types relating to Rooms of type `m.space` and related APIs
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IHierarchyRelation extends IStrippedState {
|
||||
origin_server_ts: number;
|
||||
content: {
|
||||
@@ -33,3 +34,4 @@ export interface IHierarchyRoom extends IPublicRoomsChunkRoom {
|
||||
room_type?: RoomType | string;
|
||||
children_state: IHierarchyRelation[];
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
153
node_modules/matrix-js-sdk/src/@types/state_events.ts
generated
vendored
153
node_modules/matrix-js-sdk/src/@types/state_events.ts
generated
vendored
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 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 { type RoomType } from "./event.ts";
|
||||
import { type GuestAccess, type HistoryVisibility, type JoinRule, type RestrictedAllowType } from "./partials.ts";
|
||||
import { type ImageInfo } from "./media.ts";
|
||||
import { type PolicyRecommendation } from "../models/invites-ignorer.ts";
|
||||
|
||||
export interface RoomCanonicalAliasEventContent {
|
||||
alias?: string;
|
||||
alt_aliases?: string[];
|
||||
}
|
||||
|
||||
export interface RoomCreateEventContent {
|
||||
"creator"?: string;
|
||||
"m.federate"?: boolean;
|
||||
"predecessor"?: {
|
||||
event_id: string;
|
||||
room_id: string;
|
||||
};
|
||||
"room_version"?: string;
|
||||
"type"?: RoomType;
|
||||
}
|
||||
|
||||
export interface RoomJoinRulesEventContent {
|
||||
join_rule: JoinRule;
|
||||
allow?: {
|
||||
room_id: string;
|
||||
type: RestrictedAllowType;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface RoomMemberEventContent {
|
||||
avatar_url?: string;
|
||||
displayname?: string;
|
||||
is_direct?: boolean;
|
||||
join_authorised_via_users_server?: string;
|
||||
membership: "invite" | "join" | "knock" | "leave" | "ban";
|
||||
reason?: string;
|
||||
third_party_invite?: {
|
||||
display_name: string;
|
||||
signed: {
|
||||
mxid: string;
|
||||
token: string;
|
||||
ts: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface RoomThirdPartyInviteEventContent {
|
||||
display_name: string;
|
||||
key_validity_url: string;
|
||||
public_key: string;
|
||||
public_keys: {
|
||||
key_validity_url?: string;
|
||||
public_key: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface RoomPowerLevelsEventContent {
|
||||
ban?: number;
|
||||
events?: { [eventType: string]: number };
|
||||
events_default?: number;
|
||||
invite?: number;
|
||||
kick?: number;
|
||||
notifications?: {
|
||||
room?: number;
|
||||
};
|
||||
redact?: number;
|
||||
state_default?: number;
|
||||
users?: { [userId: string]: number };
|
||||
users_default?: number;
|
||||
}
|
||||
|
||||
export interface RoomNameEventContent {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface RoomTopicEventContent {
|
||||
topic: string | undefined | null;
|
||||
}
|
||||
|
||||
export interface RoomAvatarEventContent {
|
||||
url?: string;
|
||||
// The spec says that an encrypted file can be used for the thumbnail but this isn't true
|
||||
// https://github.com/matrix-org/matrix-spec/issues/562 so omit those fields
|
||||
info?: Omit<ImageInfo, "thumbnail_file">;
|
||||
}
|
||||
|
||||
export interface RoomPinnedEventsEventContent {
|
||||
pinned: string[];
|
||||
}
|
||||
|
||||
export interface RoomEncryptionEventContent {
|
||||
"algorithm": "m.megolm.v1.aes-sha2";
|
||||
"io.element.msc4362.encrypt_state_events"?: boolean;
|
||||
"rotation_period_ms"?: number;
|
||||
"rotation_period_msgs"?: number;
|
||||
}
|
||||
|
||||
export interface RoomHistoryVisibilityEventContent {
|
||||
history_visibility: HistoryVisibility;
|
||||
}
|
||||
|
||||
export interface RoomGuestAccessEventContent {
|
||||
guest_access: GuestAccess;
|
||||
}
|
||||
|
||||
export interface RoomServerAclEventContent {
|
||||
allow?: string[];
|
||||
allow_ip_literals?: boolean;
|
||||
deny?: string[];
|
||||
}
|
||||
|
||||
export interface RoomTombstoneEventContent {
|
||||
body: string;
|
||||
replacement_room: string;
|
||||
}
|
||||
|
||||
export interface SpaceChildEventContent {
|
||||
order?: string;
|
||||
suggested?: boolean;
|
||||
via?: string[];
|
||||
}
|
||||
|
||||
export interface SpaceParentEventContent {
|
||||
canonical?: boolean;
|
||||
via?: string[];
|
||||
}
|
||||
|
||||
export interface PolicyRuleEventContent {
|
||||
entity: string;
|
||||
reason: string;
|
||||
recommendation: PolicyRecommendation;
|
||||
}
|
||||
|
||||
export interface RoomPolicyContent {
|
||||
via: string;
|
||||
public_key: string;
|
||||
}
|
||||
4
node_modules/matrix-js-sdk/src/@types/synapse.ts
generated
vendored
4
node_modules/matrix-js-sdk/src/@types/synapse.ts
generated
vendored
@@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type IdServerUnbindResult } from "./partials.ts";
|
||||
import { IdServerUnbindResult } from "./partials";
|
||||
|
||||
// Types relating to Synapse Admin APIs
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ISynapseAdminWhoisResponse {
|
||||
user_id: string;
|
||||
devices: {
|
||||
@@ -36,3 +37,4 @@ export interface ISynapseAdminWhoisResponse {
|
||||
export interface ISynapseAdminDeactivateResponse {
|
||||
id_server_unbind_result: IdServerUnbindResult;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
2
node_modules/matrix-js-sdk/src/@types/sync.ts
generated
vendored
2
node_modules/matrix-js-sdk/src/@types/sync.ts
generated
vendored
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ServerControlledNamespacedValue } from "../NamespacedValue.ts";
|
||||
import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
||||
|
||||
/**
|
||||
* https://github.com/matrix-org/matrix-doc/pull/3773
|
||||
|
||||
4
node_modules/matrix-js-sdk/src/@types/threepids.ts
generated
vendored
4
node_modules/matrix-js-sdk/src/@types/threepids.ts
generated
vendored
@@ -23,7 +23,7 @@ export enum ThreepidMedium {
|
||||
export interface IThreepid {
|
||||
medium: ThreepidMedium;
|
||||
address: string;
|
||||
validated_at: number;
|
||||
added_at: number;
|
||||
validated_at: number; // eslint-disable-line camelcase
|
||||
added_at: number; // eslint-disable-line camelcase
|
||||
bound?: boolean;
|
||||
}
|
||||
|
||||
24
node_modules/matrix-js-sdk/src/@types/topic.ts
generated
vendored
24
node_modules/matrix-js-sdk/src/@types/topic.ts
generated
vendored
@@ -13,8 +13,11 @@ 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 { NamespacedValue } from "../NamespacedValue.ts";
|
||||
import { type IMessageRendering } from "./extensible_events.ts";
|
||||
|
||||
import { EitherAnd } from "matrix-events-sdk";
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IMessageRendering } from "./extensible_events";
|
||||
|
||||
/**
|
||||
* Extensible topic event type based on MSC3765
|
||||
@@ -42,28 +45,19 @@ import { type IMessageRendering } from "./extensible_events.ts";
|
||||
/**
|
||||
* The event type for an m.topic event (in content)
|
||||
*/
|
||||
export const M_TOPIC = new NamespacedValue("m.topic", null);
|
||||
export const M_TOPIC = new UnstableValue("m.topic", "org.matrix.msc3765.topic");
|
||||
|
||||
/**
|
||||
* The event content for an m.topic event (in content)
|
||||
*/
|
||||
export type MTopicContent = { "m.text": IMessageRendering[] };
|
||||
|
||||
/**
|
||||
* A previous incorrect form of m.topic used by matrix-js-sdk
|
||||
* TODO remove this after a few releases
|
||||
* https://github.com/matrix-org/matrix-js-sdk/pull/4984#pullrequestreview-3174251065
|
||||
*/
|
||||
export type MalformedMTopicEvent = { "m.topic": IMessageRendering[] };
|
||||
export type MTopicContent = IMessageRendering[];
|
||||
|
||||
/**
|
||||
* The event definition for an m.topic event (in content)
|
||||
*/
|
||||
export type MTopicEvent = { "m.topic": MTopicContent } | MalformedMTopicEvent;
|
||||
export type MTopicEvent = EitherAnd<{ [M_TOPIC.name]: MTopicContent }, { [M_TOPIC.altName]: MTopicContent }>;
|
||||
|
||||
/**
|
||||
* The event content for an m.room.topic event
|
||||
*/
|
||||
export type MRoomTopicEventContent = {
|
||||
topic: string | null | undefined;
|
||||
} & Partial<MTopicEvent>;
|
||||
export type MRoomTopicEventContent = { topic: string } & MTopicEvent;
|
||||
|
||||
126
node_modules/matrix-js-sdk/src/@types/type-guards.ts
generated
vendored
126
node_modules/matrix-js-sdk/src/@types/type-guards.ts
generated
vendored
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 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 { logger } from "../logger.ts";
|
||||
|
||||
/**
|
||||
* Type guard to check the given value is a Record type with string keys.
|
||||
* @param value - the value to check
|
||||
*/
|
||||
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === "object" && !Array.isArray(value);
|
||||
|
||||
/**
|
||||
* Type guard to check the given record has a key and its value is a string
|
||||
* @param metadata - the record to check
|
||||
* @param key - the key to check
|
||||
*/
|
||||
export const hasRequiredStringProperty = <K extends string>(
|
||||
metadata: Record<string, unknown>,
|
||||
key: K,
|
||||
): metadata is Record<string, unknown> & Record<K, string> => {
|
||||
if (!metadata[key] || !hasOptionalStringProperty(metadata, key)) {
|
||||
logger.error(`Missing or invalid property: ${key}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check that if the given record has a key then its value is a string
|
||||
* @param metadata - the record to check
|
||||
* @param key - the key to check
|
||||
*/
|
||||
export const hasOptionalStringProperty = <K extends string>(
|
||||
metadata: Record<string, unknown>,
|
||||
key: K,
|
||||
): metadata is Record<string, unknown> & Record<K, string | undefined> => {
|
||||
if (!!metadata[key] && typeof metadata[key] !== "string") {
|
||||
logger.error(`Invalid property: ${key}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check the given record has a key and its value is a number
|
||||
* @param metadata - the record to check
|
||||
* @param key - the key to check
|
||||
*/
|
||||
export const hasRequiredNumberProperty = <K extends string>(
|
||||
metadata: Record<string, unknown>,
|
||||
key: K,
|
||||
): metadata is Record<string, unknown> & Record<K, number> => {
|
||||
if (!metadata[key] || !hasOptionalNumberProperty(metadata, key)) {
|
||||
logger.error(`Missing or invalid property: ${key}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check that if the given record has a key then its value is a number
|
||||
* @param metadata - the record to check
|
||||
* @param key - the key to check
|
||||
*/
|
||||
export const hasOptionalNumberProperty = <K extends string>(
|
||||
metadata: Record<string, unknown>,
|
||||
key: K,
|
||||
): metadata is Record<string, unknown> & Record<K, number | undefined> => {
|
||||
if (!!metadata[key] && typeof metadata[key] !== "number") {
|
||||
logger.error(`Invalid property: ${key}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check that if the given record has a key then its value is an array containing only strings
|
||||
* @param metadata - the record to check
|
||||
* @param key - the key to check
|
||||
*/
|
||||
export const optionalStringArrayProperty = <K extends string>(
|
||||
metadata: Record<string, unknown>,
|
||||
key: K,
|
||||
): metadata is Record<string, unknown> & Record<K, string[] | undefined> => {
|
||||
if (
|
||||
!!metadata[key] &&
|
||||
(!Array.isArray(metadata[key]) || !(<unknown[]>metadata[key]).every((v) => typeof v === "string"))
|
||||
) {
|
||||
logger.error(`Invalid property: ${key}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check the given record has a key and its value is an array of strings containing at least the given value
|
||||
* @param metadata - the record to check
|
||||
* @param key - the key to check
|
||||
* @param value - the value to require to be present in the array
|
||||
*/
|
||||
export const requiredArrayValue = <K extends string, V>(
|
||||
metadata: Record<string, unknown>,
|
||||
key: K,
|
||||
value: V,
|
||||
): metadata is Record<string, unknown> & Record<K, V[]> => {
|
||||
const array = metadata[key];
|
||||
if (!array || !Array.isArray(array) || !array.includes(value)) {
|
||||
logger.error(`Invalid property: ${key}. ${value} is required.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
9
node_modules/matrix-js-sdk/src/@types/uia.ts
generated
vendored
9
node_modules/matrix-js-sdk/src/@types/uia.ts
generated
vendored
@@ -14,11 +14,16 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type AuthDict } from "../interactive-auth.ts";
|
||||
import { IAuthDict, IAuthData } from "../interactive-auth";
|
||||
|
||||
/**
|
||||
* Helper type to represent HTTP request body for a UIA enabled endpoint
|
||||
*/
|
||||
export type UIARequest<T> = T & {
|
||||
auth?: AuthDict;
|
||||
auth?: IAuthDict;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper type to represent HTTP response body for a UIA enabled endpoint
|
||||
*/
|
||||
export type UIAResponse<T> = T | IAuthData;
|
||||
|
||||
15
node_modules/matrix-js-sdk/src/NamespacedValue.ts
generated
vendored
15
node_modules/matrix-js-sdk/src/NamespacedValue.ts
generated
vendored
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Optional } from "matrix-events-sdk/lib/types";
|
||||
|
||||
/**
|
||||
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix
|
||||
* is provided that the stable prefix should be used when representing the identifier.
|
||||
@@ -22,12 +24,9 @@ export class NamespacedValue<S extends string, U extends string> {
|
||||
// Stable is optional, but one of the two parameters is required, hence the weird-looking types.
|
||||
// Goal is to to have developers explicitly say there is no stable value (if applicable).
|
||||
public constructor(stable: S, unstable: U);
|
||||
public constructor(stable: S, unstable: U | null);
|
||||
public constructor(stable: null, unstable: U);
|
||||
public constructor(
|
||||
public readonly stable: S | null,
|
||||
public readonly unstable: U | null,
|
||||
) {
|
||||
public constructor(stable: S, unstable?: U);
|
||||
public constructor(stable: null | undefined, unstable: U);
|
||||
public constructor(public readonly stable?: S | null, public readonly unstable?: U) {
|
||||
if (!this.unstable && !this.stable) {
|
||||
throw new Error("One of stable or unstable values must be supplied");
|
||||
}
|
||||
@@ -60,8 +59,8 @@ export class NamespacedValue<S extends string, U extends string> {
|
||||
|
||||
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
|
||||
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
|
||||
public findIn<V>(obj: Partial<Record<NonNullable<S | U>, V>>): V | undefined {
|
||||
let val: V | undefined = undefined;
|
||||
public findIn<T>(obj: any): Optional<T> {
|
||||
let val: T | undefined = undefined;
|
||||
if (this.name) {
|
||||
val = obj?.[this.name];
|
||||
}
|
||||
|
||||
4
node_modules/matrix-js-sdk/src/ReEmitter.ts
generated
vendored
4
node_modules/matrix-js-sdk/src/ReEmitter.ts
generated
vendored
@@ -17,9 +17,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { type EventEmitter } from "events";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { type ListenerMap, type TypedEventEmitter } from "./models/typed-event-emitter.ts";
|
||||
import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
|
||||
export class ReEmitter {
|
||||
public constructor(private readonly target: EventEmitter) {}
|
||||
|
||||
48
node_modules/matrix-js-sdk/src/ToDeviceMessageQueue.ts
generated
vendored
48
node_modules/matrix-js-sdk/src/ToDeviceMessageQueue.ts
generated
vendored
@@ -14,19 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ToDeviceMessageId } from "./@types/event.ts";
|
||||
import { type Logger } from "./logger.ts";
|
||||
import { type MatrixClient, ClientEvent } from "./client.ts";
|
||||
import { type MatrixError } from "./http-api/index.ts";
|
||||
import {
|
||||
type IndexedToDeviceBatch,
|
||||
type ToDeviceBatch,
|
||||
type ToDeviceBatchWithTxnId,
|
||||
type ToDevicePayload,
|
||||
} from "./models/ToDeviceMessage.ts";
|
||||
import { MatrixScheduler } from "./scheduler.ts";
|
||||
import { SyncState } from "./sync.ts";
|
||||
import { MapWithDefault } from "./utils.ts";
|
||||
import { ToDeviceMessageId } from "./@types/event";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixClient, ClientEvent } from "./client";
|
||||
import { MatrixError } from "./http-api";
|
||||
import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage";
|
||||
import { MatrixScheduler } from "./scheduler";
|
||||
import { SyncState } from "./sync";
|
||||
import { MapWithDefault } from "./utils";
|
||||
|
||||
const MAX_BATCH_SIZE = 20;
|
||||
|
||||
@@ -40,10 +35,7 @@ export class ToDeviceMessageQueue {
|
||||
private retryTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryAttempts = 0;
|
||||
|
||||
public constructor(
|
||||
private client: MatrixClient,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
public constructor(private client: MatrixClient) {}
|
||||
|
||||
public start(): void {
|
||||
this.running = true;
|
||||
@@ -70,14 +62,14 @@ export class ToDeviceMessageQueue {
|
||||
const msgmap = batchWithTxnId.batch.map(
|
||||
(msg) => `${msg.userId}/${msg.deviceId} (msgid ${msg.payload[ToDeviceMessageId]})`,
|
||||
);
|
||||
this.logger.info(
|
||||
logger.info(
|
||||
`Enqueuing batch of to-device messages. type=${batch.eventType} txnid=${batchWithTxnId.txnId}`,
|
||||
msgmap,
|
||||
);
|
||||
}
|
||||
|
||||
await this.client.store.saveToDeviceBatches(batches);
|
||||
void this.sendQueue();
|
||||
this.sendQueue();
|
||||
}
|
||||
|
||||
public sendQueue = async (): Promise<void> => {
|
||||
@@ -86,7 +78,7 @@ export class ToDeviceMessageQueue {
|
||||
|
||||
if (this.sending || !this.running) return;
|
||||
|
||||
this.logger.debug("Attempting to send queued to-device messages");
|
||||
logger.debug("Attempting to send queued to-device messages");
|
||||
|
||||
this.sending = true;
|
||||
let headBatch: IndexedToDeviceBatch | null;
|
||||
@@ -102,23 +94,25 @@ export class ToDeviceMessageQueue {
|
||||
// Make sure we're still running after the async tasks: if not, stop.
|
||||
if (!this.running) return;
|
||||
|
||||
this.logger.debug("All queued to-device messages sent");
|
||||
logger.debug("All queued to-device messages sent");
|
||||
} catch (e) {
|
||||
++this.retryAttempts;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
// eslint-disable-next-line new-cap
|
||||
const retryDelay = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(null, this.retryAttempts, <MatrixError>e);
|
||||
if (retryDelay === -1) {
|
||||
// the scheduler function doesn't differentiate between fatal errors and just getting
|
||||
// bored and giving up for now
|
||||
if (Math.floor((<MatrixError>e).httpStatus! / 100) === 4) {
|
||||
this.logger.error("Fatal error when sending to-device message - dropping to-device batch!", e);
|
||||
logger.error("Fatal error when sending to-device message - dropping to-device batch!", e);
|
||||
await this.client.store.removeToDeviceBatch(headBatch!.id);
|
||||
} else {
|
||||
this.logger.info("Automatic retry limit reached for to-device messages.");
|
||||
logger.info("Automatic retry limit reached for to-device messages.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(`Failed to send batch of to-device messages. Will retry in ${retryDelay}ms`, e);
|
||||
logger.info(`Failed to send batch of to-device messages. Will retry in ${retryDelay}ms`, e);
|
||||
this.retryTimeout = setTimeout(this.sendQueue, retryDelay);
|
||||
} finally {
|
||||
this.sending = false;
|
||||
@@ -134,7 +128,7 @@ export class ToDeviceMessageQueue {
|
||||
contentMap.getOrCreate(item.userId).set(item.deviceId, item.payload);
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
logger.info(
|
||||
`Sending batch of ${batch.batch.length} to-device messages with ID ${batch.id} and txnId ${batch.txnId}`,
|
||||
);
|
||||
|
||||
@@ -147,8 +141,8 @@ export class ToDeviceMessageQueue {
|
||||
*/
|
||||
private onResumedSync = (state: SyncState | null, oldState: SyncState | null): void => {
|
||||
if (state === SyncState.Syncing && oldState !== SyncState.Syncing) {
|
||||
this.logger.info(`Resuming queue after resumed sync`);
|
||||
void this.sendQueue();
|
||||
logger.info(`Resuming queue after resumed sync`);
|
||||
this.sendQueue();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
179
node_modules/matrix-js-sdk/src/autodiscovery.ts
generated
vendored
179
node_modules/matrix-js-sdk/src/autodiscovery.ts
generated
vendored
@@ -15,10 +15,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type IClientWellKnown, type IWellKnownConfig, type IServerVersions } from "./client.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
import { type MatrixError, Method, timeoutSignal } from "./http-api/index.ts";
|
||||
import { SUPPORTED_MATRIX_VERSIONS } from "./version-support.ts";
|
||||
import { SigningKey } from "oidc-client-ts";
|
||||
|
||||
import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, IServerVersions, M_AUTHENTICATION } from "./client";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixError, Method, timeoutSignal } from "./http-api";
|
||||
import { discoverAndValidateAuthenticationConfig } from "./oidc/discovery";
|
||||
import {
|
||||
ValidatedIssuerConfig,
|
||||
ValidatedIssuerMetadata,
|
||||
validateOIDCIssuerWellKnown,
|
||||
validateWellKnownAuthentication,
|
||||
} from "./oidc/validate";
|
||||
import { OidcError } from "./oidc/error";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
@@ -31,7 +40,7 @@ export enum AutoDiscoveryAction {
|
||||
FAIL_ERROR = "FAIL_ERROR",
|
||||
}
|
||||
|
||||
export enum AutoDiscoveryError {
|
||||
enum AutoDiscoveryError {
|
||||
Invalid = "Invalid homeserver discovery response",
|
||||
GenericFailure = "Failed to get autodiscovery configuration from server",
|
||||
InvalidHsBaseUrl = "Invalid base_url for m.homeserver",
|
||||
@@ -41,10 +50,6 @@ export enum AutoDiscoveryError {
|
||||
InvalidIs = "Invalid identity server discovery response",
|
||||
MissingWellknown = "No .well-known JSON file found",
|
||||
InvalidJson = "Invalid JSON",
|
||||
UnsupportedHomeserverSpecVersion = "The homeserver does not meet the 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 {
|
||||
@@ -53,9 +58,26 @@ interface AutoDiscoveryState {
|
||||
}
|
||||
interface WellKnownConfig extends Omit<IWellKnownConfig, "error">, AutoDiscoveryState {}
|
||||
|
||||
/**
|
||||
* @deprecated in favour of OidcClientConfig
|
||||
*/
|
||||
interface DelegatedAuthConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig, AutoDiscoveryState {}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export interface OidcClientConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig {
|
||||
metadata: ValidatedIssuerMetadata;
|
||||
signingKeys?: SigningKey[];
|
||||
}
|
||||
|
||||
export interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
|
||||
"m.homeserver": WellKnownConfig;
|
||||
"m.identity_server": WellKnownConfig;
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
"m.authentication"?: (OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,10 +108,7 @@ export class AutoDiscovery {
|
||||
|
||||
public static readonly ERROR_INVALID_JSON = AutoDiscoveryError.InvalidJson;
|
||||
|
||||
public static readonly ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION =
|
||||
AutoDiscoveryError.UnsupportedHomeserverSpecVersion;
|
||||
|
||||
public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError) as AutoDiscoveryError[];
|
||||
public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError);
|
||||
|
||||
/**
|
||||
* The auto discovery failed. The client is expected to communicate
|
||||
@@ -130,7 +149,7 @@ export class AutoDiscovery {
|
||||
* configuration, which may include error states. Rejects on unexpected
|
||||
* failure, not when verification fails.
|
||||
*/
|
||||
public static async fromDiscoveryConfig(wellknown?: IClientWellKnown): Promise<ClientConfig> {
|
||||
public static async fromDiscoveryConfig(wellknown: IClientWellKnown): Promise<ClientConfig> {
|
||||
// Step 1 is to get the config, which is provided to us here.
|
||||
|
||||
// We default to an error state to make the first few checks easier to
|
||||
@@ -180,7 +199,7 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 3: Make sure the homeserver URL points to a homeserver.
|
||||
const hsVersions = await this.fetchWellKnownObject<IServerVersions>(`${hsUrl}/_matrix/client/versions`);
|
||||
if (!hsVersions || !Array.isArray(hsVersions.raw?.["versions"])) {
|
||||
if (!hsVersions?.raw?.["versions"]) {
|
||||
logger.error("Invalid /versions response");
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
|
||||
|
||||
@@ -191,27 +210,6 @@ export class AutoDiscovery {
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
// Step 3.1: Non-spec check to ensure the server will actually work for us. We need to check if
|
||||
// any of the versions in `SUPPORTED_MATRIX_VERSIONS` are listed in the /versions response.
|
||||
const hsVersionSet = new Set(hsVersions.raw["versions"]);
|
||||
let supportedVersionFound = false;
|
||||
for (const version of SUPPORTED_MATRIX_VERSIONS) {
|
||||
if (hsVersionSet.has(version)) {
|
||||
supportedVersionFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!supportedVersionFound) {
|
||||
logger.error("Homeserver does not meet version requirements");
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION;
|
||||
|
||||
// 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,
|
||||
@@ -286,10 +284,107 @@ export class AutoDiscovery {
|
||||
}
|
||||
});
|
||||
|
||||
const authConfig = await this.discoverAndValidateAuthenticationConfig(wellknown);
|
||||
clientConfig[M_AUTHENTICATION.stable!] = authConfig;
|
||||
|
||||
// Step 8: Give the config to the caller (finally)
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate delegated auth configuration
|
||||
* @deprecated use discoverAndValidateAuthenticationConfig
|
||||
* - m.authentication config is present and valid
|
||||
* - delegated auth issuer openid-configuration is reachable
|
||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
||||
* When successful, DelegatedAuthConfig will be returned with endpoints used for delegated auth
|
||||
* Any errors are caught, and AutoDiscoveryState returned with error
|
||||
* @param wellKnown - configuration object as returned
|
||||
* by the .well-known auto-discovery endpoint
|
||||
* @returns Config or failure result
|
||||
*/
|
||||
public static async validateDiscoveryAuthenticationConfig(
|
||||
wellKnown: IClientWellKnown,
|
||||
): Promise<DelegatedAuthConfig | AutoDiscoveryState> {
|
||||
try {
|
||||
const authentication = M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown) || undefined;
|
||||
const homeserverAuthenticationConfig = validateWellKnownAuthentication(authentication);
|
||||
|
||||
const issuerOpenIdConfigUrl = `${this.sanitizeWellKnownUrl(
|
||||
homeserverAuthenticationConfig.issuer,
|
||||
)}/.well-known/openid-configuration`;
|
||||
const issuerWellKnown = await this.fetchWellKnownObject<unknown>(issuerOpenIdConfigUrl);
|
||||
|
||||
if (issuerWellKnown.action !== AutoDiscoveryAction.SUCCESS) {
|
||||
logger.error("Failed to fetch issuer openid configuration");
|
||||
throw new Error(OidcError.General);
|
||||
}
|
||||
|
||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown.raw);
|
||||
|
||||
const delegatedAuthConfig: DelegatedAuthConfig = {
|
||||
state: AutoDiscoveryAction.SUCCESS,
|
||||
error: null,
|
||||
...homeserverAuthenticationConfig,
|
||||
...validatedIssuerConfig,
|
||||
};
|
||||
return delegatedAuthConfig;
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message as unknown as OidcError;
|
||||
const errorType = Object.values(OidcError).includes(errorMessage) ? errorMessage : OidcError.General;
|
||||
|
||||
const state =
|
||||
errorType === OidcError.NotSupported ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR;
|
||||
|
||||
return {
|
||||
state,
|
||||
error: errorType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate delegated auth configuration
|
||||
* - m.authentication config is present and valid
|
||||
* - delegated auth issuer openid-configuration is reachable
|
||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
||||
* When successful, validated authentication metadata and optionally signing keys will be returned
|
||||
* Any errors are caught, and AutoDiscoveryState returned with error
|
||||
* @param wellKnown - configuration object as returned
|
||||
* by the .well-known auto-discovery endpoint
|
||||
* @returns Config or failure result
|
||||
*/
|
||||
public static async discoverAndValidateAuthenticationConfig(
|
||||
wellKnown: IClientWellKnown,
|
||||
): Promise<(OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState> {
|
||||
try {
|
||||
const authentication = M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown) || undefined;
|
||||
const result = await discoverAndValidateAuthenticationConfig(authentication);
|
||||
|
||||
// include this for backwards compatibility
|
||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(result.metadata);
|
||||
|
||||
const response = {
|
||||
state: AutoDiscoveryAction.SUCCESS,
|
||||
error: null,
|
||||
...validatedIssuerConfig,
|
||||
...result,
|
||||
};
|
||||
return response;
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message as unknown as OidcError;
|
||||
const errorType = Object.values(OidcError).includes(errorMessage) ? errorMessage : OidcError.General;
|
||||
|
||||
const state =
|
||||
errorType === OidcError.NotSupported ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR;
|
||||
|
||||
return {
|
||||
state,
|
||||
error: errorType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to automatically discover client configuration information
|
||||
* prior to logging in. Such information includes the homeserver URL
|
||||
@@ -358,7 +453,7 @@ export class AutoDiscovery {
|
||||
}
|
||||
|
||||
// Step 2: Validate and parse the config
|
||||
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw);
|
||||
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw!);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -414,16 +509,16 @@ export class AutoDiscovery {
|
||||
}
|
||||
}
|
||||
|
||||
private static fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof globalThis.fetch> {
|
||||
private static fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
|
||||
if (this.fetchFn) {
|
||||
return this.fetchFn(resource, options);
|
||||
}
|
||||
return globalThis.fetch(resource, options);
|
||||
return global.fetch(resource, options);
|
||||
}
|
||||
|
||||
private static fetchFn?: typeof globalThis.fetch;
|
||||
private static fetchFn?: typeof global.fetch;
|
||||
|
||||
public static setFetchFn(fetchFn: typeof globalThis.fetch): void {
|
||||
public static setFetchFn(fetchFn: typeof global.fetch): void {
|
||||
AutoDiscovery.fetchFn = fetchFn;
|
||||
}
|
||||
|
||||
@@ -462,7 +557,7 @@ export class AutoDiscovery {
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
raw: {},
|
||||
action: AutoDiscoveryAction.FAIL_PROMPT,
|
||||
|
||||
86
node_modules/matrix-js-sdk/src/base64.ts
generated
vendored
86
node_modules/matrix-js-sdk/src/base64.ts
generated
vendored
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
Copyright 2023 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base64 encoding and decoding utilities
|
||||
*/
|
||||
|
||||
function toBase64(uint8Array: Uint8Array, options: Uint8ArrayToBase64Options): string {
|
||||
if (typeof uint8Array.toBase64 === "function") {
|
||||
// Currently this is only supported in Firefox,
|
||||
// but we match the options in the hope in the future we can rely on it for all environments.
|
||||
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
|
||||
return uint8Array.toBase64(options);
|
||||
}
|
||||
|
||||
let base64 = btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), ""));
|
||||
if (options.omitPadding) {
|
||||
base64 = base64.replace(/={1,2}$/, "");
|
||||
}
|
||||
if (options.alphabet === "base64url") {
|
||||
base64 = base64.replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a typed array of uint8 as base64.
|
||||
* @param uint8Array - The data to encode.
|
||||
* @returns The base64.
|
||||
*/
|
||||
export function encodeBase64(uint8Array: Uint8Array): string {
|
||||
return toBase64(uint8Array, { alphabet: "base64", omitPadding: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a typed array of uint8 as unpadded base64.
|
||||
* @param uint8Array - The data to encode.
|
||||
* @returns The unpadded base64.
|
||||
*/
|
||||
export function encodeUnpaddedBase64(uint8Array: Uint8Array): string {
|
||||
return toBase64(uint8Array, { alphabet: "base64", omitPadding: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a typed array of uint8 as unpadded base64 using the URL-safe encoding.
|
||||
* @param uint8Array - The data to encode.
|
||||
* @returns The unpadded base64.
|
||||
*/
|
||||
export function encodeUnpaddedBase64Url(uint8Array: Uint8Array): string {
|
||||
return toBase64(uint8Array, { alphabet: "base64url", omitPadding: true });
|
||||
}
|
||||
|
||||
function fromBase64(base64: string, options: Uint8ArrayFromBase64Options): Uint8Array<ArrayBuffer> {
|
||||
if (typeof Uint8Array.fromBase64 === "function") {
|
||||
// Currently this is only supported in Firefox,
|
||||
// but we match the options in the hope in the future we can rely on it for all environments.
|
||||
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64
|
||||
return Uint8Array.fromBase64(base64, options);
|
||||
}
|
||||
|
||||
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a base64 (or base64url) string to a typed array of uint8.
|
||||
* @param base64 - The base64 to decode.
|
||||
* @returns The decoded data.
|
||||
*/
|
||||
export function decodeBase64(base64: string): Uint8Array<ArrayBuffer> {
|
||||
// The function requires us to select an alphabet, but we don't know if base64url was used so we convert.
|
||||
return fromBase64(base64.replace(/-/g, "+").replace(/_/g, "/"), { alphabet: "base64", lastChunkHandling: "loose" });
|
||||
}
|
||||
25
node_modules/matrix-js-sdk/src/browser-index.ts
generated
vendored
25
node_modules/matrix-js-sdk/src/browser-index.ts
generated
vendored
@@ -14,31 +14,34 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as sdk from "./matrix.ts";
|
||||
import * as matrixcs from "./matrix";
|
||||
|
||||
type BrowserMatrix = typeof sdk;
|
||||
type BrowserMatrix = typeof matrixcs;
|
||||
declare global {
|
||||
/* eslint-disable no-var, camelcase */
|
||||
var __js_sdk_entrypoint: boolean;
|
||||
var matrixcs: BrowserMatrix;
|
||||
/* eslint-enable no-var */
|
||||
}
|
||||
|
||||
if (globalThis.__js_sdk_entrypoint) {
|
||||
if (global.__js_sdk_entrypoint) {
|
||||
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
|
||||
}
|
||||
globalThis.__js_sdk_entrypoint = true;
|
||||
global.__js_sdk_entrypoint = true;
|
||||
|
||||
// just *accessing* indexedDB throws an exception in firefox with indexeddb disabled.
|
||||
let indexedDB: IDBFactory | undefined;
|
||||
try {
|
||||
indexedDB = globalThis.indexedDB;
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
indexedDB = global.indexedDB;
|
||||
} catch (e) {}
|
||||
|
||||
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
|
||||
if (indexedDB) {
|
||||
sdk.setCryptoStoreFactory(() => new sdk.IndexedDBCryptoStore(indexedDB, "matrix-js-sdk:crypto"));
|
||||
matrixcs.setCryptoStoreFactory(() => new matrixcs.IndexedDBCryptoStore(indexedDB!, "matrix-js-sdk:crypto"));
|
||||
}
|
||||
|
||||
export * from "./matrix.ts";
|
||||
globalThis.matrixcs = sdk;
|
||||
// We export 3 things to make browserify happy as well as downstream projects.
|
||||
// It's awkward, but required.
|
||||
export * from "./matrix";
|
||||
export default matrixcs; // keep export for browserify package deps
|
||||
global.matrixcs = matrixcs;
|
||||
|
||||
102
node_modules/matrix-js-sdk/src/capabilityPoller.ts
generated
vendored
102
node_modules/matrix-js-sdk/src/capabilityPoller.ts
generated
vendored
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2024 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 { type IHttpOpts, type MatrixHttpApi } from "./http-api/index.ts";
|
||||
import { type Logger } from "./logger.ts";
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter.ts";
|
||||
import { deepCompare } from "./utils.ts";
|
||||
|
||||
// How often we update the server capabilities.
|
||||
// 6 hours - an arbitrary value, but they should change very infrequently.
|
||||
const CAPABILITIES_CACHE_MS = 6 * 60 * 60 * 1000;
|
||||
|
||||
// How long we want before retrying if we couldn't fetch
|
||||
const CAPABILITIES_RETRY_MS = 30 * 1000;
|
||||
|
||||
/**
|
||||
* Manages storing and periodically refreshing the server capabilities.
|
||||
*/
|
||||
export abstract class CapabilityPoller<ResponseType> extends TypedEventEmitter<
|
||||
"update",
|
||||
{ update: (data: ResponseType) => void }
|
||||
> {
|
||||
protected cached?: ResponseType;
|
||||
private retryTimeout?: ReturnType<typeof setTimeout>;
|
||||
private refreshTimeout?: ReturnType<typeof setInterval>;
|
||||
|
||||
public constructor(
|
||||
protected readonly logger: Logger,
|
||||
protected readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
private readonly name: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts periodically fetching the server capabilities.
|
||||
*/
|
||||
public start(): void {
|
||||
this.poll().then();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the service
|
||||
*/
|
||||
public stop(): void {
|
||||
this.clearTimeouts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached capabilities, or undefined if none are cached.
|
||||
* @returns the current capabilities, if any.
|
||||
*/
|
||||
public getCached(): ResponseType | undefined {
|
||||
return this.cached;
|
||||
}
|
||||
|
||||
public abstract fetch(): Promise<ResponseType>;
|
||||
|
||||
private poll = async (): Promise<void> => {
|
||||
this.logger.debug("Checking capabilites");
|
||||
try {
|
||||
const current = this.cached;
|
||||
await this.fetch();
|
||||
this.clearTimeouts();
|
||||
this.refreshTimeout = globalThis.setTimeout(this.poll, CAPABILITIES_CACHE_MS);
|
||||
this.logger.debug(`Fetched new server ${this.name}`);
|
||||
if (this.cached && !deepCompare(current, this.cached)) {
|
||||
this.emit("update", this.cached);
|
||||
}
|
||||
} catch (e) {
|
||||
this.clearTimeouts();
|
||||
const howLong = Math.floor(CAPABILITIES_RETRY_MS + Math.random() * 5000);
|
||||
this.retryTimeout = globalThis.setTimeout(this.poll, howLong);
|
||||
this.logger.warn(`Failed to refresh ${this.name}: retrying in ${howLong}ms`, e);
|
||||
}
|
||||
};
|
||||
|
||||
private clearTimeouts(): void {
|
||||
if (this.refreshTimeout) {
|
||||
clearInterval(this.refreshTimeout);
|
||||
this.refreshTimeout = undefined;
|
||||
}
|
||||
if (this.retryTimeout) {
|
||||
clearTimeout(this.retryTimeout);
|
||||
this.retryTimeout = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
5584
node_modules/matrix-js-sdk/src/client.ts
generated
vendored
5584
node_modules/matrix-js-sdk/src/client.ts
generated
vendored
File diff suppressed because it is too large
Load Diff
206
node_modules/matrix-js-sdk/src/common-crypto/CryptoBackend.ts
generated
vendored
206
node_modules/matrix-js-sdk/src/common-crypto/CryptoBackend.ts
generated
vendored
@@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDeviceLists, IToDeviceEvent, ReceivedToDeviceMessage } from "../sync-accumulator.ts";
|
||||
import { type IClearEvent, type MatrixEvent } from "../models/event.ts";
|
||||
import { type Room } from "../models/room.ts";
|
||||
import { type CryptoApi, type DecryptionFailureCode, type ImportRoomKeysOpts } from "../crypto-api/index.ts";
|
||||
import { type KeyBackupInfo, type KeyBackupSession } from "../crypto-api/keybackup.ts";
|
||||
import { type IMegolmSessionData } from "../@types/crypto.ts";
|
||||
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { CryptoApi } from "../crypto-api";
|
||||
import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { IEncryptedEventInfo } from "../crypto/api";
|
||||
import { IEventDecryptionResult } from "../@types/crypto";
|
||||
|
||||
/**
|
||||
* Common interface for the crypto implementations
|
||||
@@ -33,8 +34,8 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
* symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
|
||||
* no room-level equivalent for this setting.
|
||||
*
|
||||
* @remarks This has no effect in Rust Crypto; it exists only for the sake of
|
||||
* the accessors in MatrixClient.
|
||||
* @remarks this is here, rather than in `CryptoApi`, because I don't think we're
|
||||
* going to support it in the rust crypto implementation.
|
||||
*/
|
||||
globalErrorOnUnknownDevices: boolean;
|
||||
|
||||
@@ -43,6 +44,15 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
*/
|
||||
stop(): void;
|
||||
|
||||
/**
|
||||
* Get the verification level for a given user
|
||||
*
|
||||
* TODO: define this better
|
||||
*
|
||||
* @param userId - user to be checked
|
||||
*/
|
||||
checkUserTrust(userId: string): UserTrustLevel;
|
||||
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room.
|
||||
*
|
||||
@@ -61,61 +71,34 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
* @returns a promise which resolves once we have finished decrypting.
|
||||
* Rejects with an error if there is a problem decrypting the event.
|
||||
*/
|
||||
decryptEvent(event: MatrixEvent): Promise<EventDecryptionResult>;
|
||||
decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult>;
|
||||
|
||||
/**
|
||||
* Get a backup decryptor capable of decrypting megolm session data encrypted with the given backup information.
|
||||
* @param backupInfo - The backup information
|
||||
* @param privKey - The private decryption key.
|
||||
* Get information about the encryption of an event
|
||||
*
|
||||
* @param event - event to be checked
|
||||
*/
|
||||
getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise<BackupDecryptor>;
|
||||
getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo;
|
||||
|
||||
/**
|
||||
* Import a list of room keys restored from backup
|
||||
* Get the cross signing information for a given user.
|
||||
*
|
||||
* @param keys - a list of session export objects
|
||||
* @param backupVersion - the version of the backup these keys came from.
|
||||
* @param opts - options object
|
||||
* @returns a promise which resolves once the keys have been imported
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param userId - the user ID to get the cross-signing info for.
|
||||
*
|
||||
* @returns the cross signing information for the user.
|
||||
*/
|
||||
importBackedUpRoomKeys(keys: IMegolmSessionData[], backupVersion: string, opts?: ImportRoomKeysOpts): Promise<void>;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Room key history sharing (MSC4268)
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null;
|
||||
|
||||
/**
|
||||
* Share any shareable E2EE history in the given room with the given recipient,
|
||||
* as per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
|
||||
* Check the cross signing trust of the current user
|
||||
*
|
||||
* @param opts - Options object.
|
||||
*
|
||||
* @deprecated Unneeded for the new crypto
|
||||
*/
|
||||
shareRoomHistoryWithUser(roomId: string, userId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Having accepted an invite for the given room from the given user, attempt to
|
||||
* find information about a room key bundle and, if found, download the
|
||||
* bundle and import the room keys, as per {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4268|MSC4268}.
|
||||
*
|
||||
* @param roomId - The room we were invited to, for which we want to check if a room
|
||||
* key bundle was received.
|
||||
*
|
||||
* @param inviter - The user who invited us to the room and is expected to have
|
||||
* sent the room key bundle.
|
||||
*
|
||||
* @returns `true` if the key bundle was successfuly downloaded and imported.
|
||||
*/
|
||||
maybeAcceptKeyBundle(roomId: string, inviter: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Mark a room as pending a key bundle under MSC4268. The backend will listen for room key bundle messages, and if
|
||||
* it sees one matching the room specified, it will automatically import it as long as the message author's ID matches
|
||||
* the inviter's ID.
|
||||
*
|
||||
* @param roomId - The room we were invited to, for which we did not receive a key bundle before accepting the invite.
|
||||
* @param inviterId - The user who invited us to the room and is expected to send the room key bundle.
|
||||
*/
|
||||
markRoomAsPendingKeyBundle(roomId: string, inviterId: string): Promise<void>;
|
||||
checkOwnCrossSigningTrust(opts?: CheckOwnCrossSigningTrustOpts): Promise<void>;
|
||||
}
|
||||
|
||||
/** The methods which crypto implementations should expose to the Sync api
|
||||
@@ -133,11 +116,9 @@ export interface SyncCryptoCallbacks {
|
||||
* messages, rather than the results of any decryption attempts.
|
||||
*
|
||||
* @param events - the received to-device messages
|
||||
* @returns A list of preprocessed to-device messages. This will not map 1:1 to the input list, as some messages may be invalid or
|
||||
* failed to decrypt, and so will be omitted from the output list.
|
||||
*
|
||||
* @returns A list of preprocessed to-device messages.
|
||||
*/
|
||||
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<ReceivedToDeviceMessage[]>;
|
||||
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop when one time key counts and unused fallback key details are received.
|
||||
@@ -177,15 +158,6 @@ export interface SyncCryptoCallbacks {
|
||||
* @param syncState - information about the completed sync.
|
||||
*/
|
||||
onSyncCompleted(syncState: OnSyncCompletedData): void;
|
||||
|
||||
/**
|
||||
* Mark all tracked users' device lists as dirty.
|
||||
*
|
||||
* This method will cause additional `/keys/query` requests on the server, so should be used only
|
||||
* when the client has desynced tracking device list deltas from the server.
|
||||
* In MSC4186: Simplified Sliding Sync, this can happen when the server expires the connection.
|
||||
*/
|
||||
markAllTrackedUsersAsDirty(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,104 +176,8 @@ export interface OnSyncCompletedData {
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a (successful) call to {@link CryptoBackend.decryptEvent}
|
||||
* Options object for {@link CryptoBackend#checkOwnCrossSigningTrust}.
|
||||
*/
|
||||
export interface EventDecryptionResult {
|
||||
/**
|
||||
* The plaintext payload for the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
|
||||
*/
|
||||
clearEvent: IClearEvent;
|
||||
/**
|
||||
* No longer used.
|
||||
* See {@link MatrixEvent#getForwardingCurve25519KeyChain}.
|
||||
* @deprecated
|
||||
*/
|
||||
forwardingCurve25519KeyChain?: string[];
|
||||
/**
|
||||
* Key owned by the sender of this event. See {@link MatrixEvent#getSenderKey}.
|
||||
*/
|
||||
senderCurve25519Key?: string;
|
||||
/**
|
||||
* ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}.
|
||||
*/
|
||||
claimedEd25519Key?: string;
|
||||
|
||||
/**
|
||||
* If another user forwarded the key to this message
|
||||
* (eg via [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)),
|
||||
* the ID of that user.
|
||||
*/
|
||||
keyForwardedBy?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for decrypting megolm session data retrieved from a remote backup.
|
||||
* The result of {@link CryptoBackend#getBackupDecryptor}.
|
||||
*/
|
||||
export interface BackupDecryptor {
|
||||
/**
|
||||
* Whether keys retrieved from this backup can be trusted.
|
||||
*
|
||||
* Depending on the backup algorithm, keys retrieved from the backup can be trusted or not.
|
||||
* If false, keys retrieved from the backup must be considered unsafe (authenticity cannot be guaranteed).
|
||||
* It could be by design (deniability) or for some technical reason (eg asymmetric encryption).
|
||||
*/
|
||||
readonly sourceTrusted: boolean;
|
||||
|
||||
/**
|
||||
*
|
||||
* Decrypt megolm session data retrieved from backup.
|
||||
*
|
||||
* @param ciphertexts - a Record of sessionId to session data.
|
||||
*
|
||||
* @returns An array of decrypted `IMegolmSessionData`
|
||||
*/
|
||||
decryptSessions(ciphertexts: Record<string, KeyBackupSession>): Promise<IMegolmSessionData[]>;
|
||||
|
||||
/**
|
||||
* Free any resources held by this decryptor.
|
||||
*
|
||||
* Should be called once the decryptor is no longer needed.
|
||||
*/
|
||||
free(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown when decryption fails
|
||||
*
|
||||
* @param code - Reason code for the failure.
|
||||
*
|
||||
* @param msg - user-visible message describing the problem
|
||||
*
|
||||
* @param details - key/value pairs reported in the logs but not shown
|
||||
* to the user.
|
||||
*/
|
||||
export class DecryptionError extends Error {
|
||||
public readonly detailedString: string;
|
||||
|
||||
public constructor(
|
||||
public readonly code: DecryptionFailureCode,
|
||||
msg: string,
|
||||
details?: Record<string, string | Error>,
|
||||
) {
|
||||
super(msg);
|
||||
this.name = "DecryptionError";
|
||||
this.detailedString = detailedStringForDecryptionError(this, details);
|
||||
}
|
||||
}
|
||||
|
||||
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string | Error>): string {
|
||||
let result = err.name + "[msg: " + err.message;
|
||||
|
||||
if (details) {
|
||||
result +=
|
||||
", " +
|
||||
Object.keys(details)
|
||||
.map((k) => k + ": " + details[k])
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
result += "]";
|
||||
|
||||
return result;
|
||||
export interface CheckOwnCrossSigningTrustOpts {
|
||||
allowPrivateKeyRequests?: boolean;
|
||||
}
|
||||
|
||||
42
node_modules/matrix-js-sdk/src/common-crypto/key-passphrase.ts
generated
vendored
42
node_modules/matrix-js-sdk/src/common-crypto/key-passphrase.ts
generated
vendored
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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 { deriveRecoveryKeyFromPassphrase } from "../crypto-api/index.ts";
|
||||
|
||||
interface IAuthData {
|
||||
private_key_salt?: string;
|
||||
private_key_iterations?: number;
|
||||
private_key_bits?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a backup key from a passphrase using the salt and iterations from the auth data.
|
||||
* @param authData - The auth data containing the salt and iterations
|
||||
* @param passphrase - The passphrase to derive the key from
|
||||
* @deprecated Deriving a backup key from a passphrase is not part of the matrix spec. Instead, a random key is generated and stored/shared via 4S.
|
||||
*/
|
||||
export function keyFromAuthData(authData: IAuthData, passphrase: string): Promise<Uint8Array> {
|
||||
if (!authData.private_key_salt || !authData.private_key_iterations) {
|
||||
throw new Error("Salt and/or iterations not found: this backup cannot be restored with a passphrase");
|
||||
}
|
||||
|
||||
return deriveRecoveryKeyFromPassphrase(
|
||||
passphrase,
|
||||
authData.private_key_salt,
|
||||
authData.private_key_iterations,
|
||||
authData.private_key_bits,
|
||||
);
|
||||
}
|
||||
62
node_modules/matrix-js-sdk/src/content-helpers.ts
generated
vendored
62
node_modules/matrix-js-sdk/src/content-helpers.ts
generated
vendored
@@ -14,23 +14,23 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MBeaconEventContent, type MBeaconInfoContent, type MBeaconInfoEventContent } from "./@types/beacon.ts";
|
||||
import { MsgType } from "./@types/event.ts";
|
||||
import { type IMessageRendering, M_TEXT, REFERENCE_RELATION } from "./@types/extensible_events.ts";
|
||||
import { isProvided } from "./extensible_events_v1/utilities.ts";
|
||||
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon";
|
||||
import { MsgType } from "./@types/event";
|
||||
import { M_TEXT, REFERENCE_RELATION } from "./@types/extensible_events";
|
||||
import { isProvided } from "./extensible_events_v1/utilities";
|
||||
import {
|
||||
M_ASSET,
|
||||
LocationAssetType,
|
||||
M_LOCATION,
|
||||
M_TIMESTAMP,
|
||||
type LocationEventWireContent,
|
||||
type MLocationEventContent,
|
||||
type MLocationContent,
|
||||
type MAssetContent,
|
||||
type LegacyLocationEventContent,
|
||||
} from "./@types/location.ts";
|
||||
import { type MRoomTopicEventContent, type MTopicContent, M_TOPIC, type MTopicEvent } from "./@types/topic.ts";
|
||||
import { type RoomMessageEventContent } from "./@types/events.ts";
|
||||
LocationEventWireContent,
|
||||
MLocationEventContent,
|
||||
MLocationContent,
|
||||
MAssetContent,
|
||||
LegacyLocationEventContent,
|
||||
} from "./@types/location";
|
||||
import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic";
|
||||
import { IContent } from "./models/event";
|
||||
|
||||
/**
|
||||
* Generates the content for a HTML Message event
|
||||
@@ -38,7 +38,7 @@ import { type RoomMessageEventContent } from "./@types/events.ts";
|
||||
* @param htmlBody - the HTML representation of the message
|
||||
* @returns
|
||||
*/
|
||||
export function makeHtmlMessage(body: string, htmlBody: string): RoomMessageEventContent {
|
||||
export function makeHtmlMessage(body: string, htmlBody: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Text,
|
||||
format: "org.matrix.custom.html",
|
||||
@@ -53,7 +53,7 @@ export function makeHtmlMessage(body: string, htmlBody: string): RoomMessageEven
|
||||
* @param htmlBody - the HTML representation of the notice
|
||||
* @returns
|
||||
*/
|
||||
export function makeHtmlNotice(body: string, htmlBody: string): RoomMessageEventContent {
|
||||
export function makeHtmlNotice(body: string, htmlBody: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Notice,
|
||||
format: "org.matrix.custom.html",
|
||||
@@ -68,7 +68,7 @@ export function makeHtmlNotice(body: string, htmlBody: string): RoomMessageEvent
|
||||
* @param htmlBody - the HTML representation of the emote
|
||||
* @returns
|
||||
*/
|
||||
export function makeHtmlEmote(body: string, htmlBody: string): RoomMessageEventContent {
|
||||
export function makeHtmlEmote(body: string, htmlBody: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Emote,
|
||||
format: "org.matrix.custom.html",
|
||||
@@ -82,7 +82,7 @@ export function makeHtmlEmote(body: string, htmlBody: string): RoomMessageEventC
|
||||
* @param body - the plaintext body of the emote
|
||||
* @returns
|
||||
*/
|
||||
export function makeTextMessage(body: string): RoomMessageEventContent {
|
||||
export function makeTextMessage(body: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Text,
|
||||
body: body,
|
||||
@@ -94,7 +94,7 @@ export function makeTextMessage(body: string): RoomMessageEventContent {
|
||||
* @param body - the plaintext body of the notice
|
||||
* @returns
|
||||
*/
|
||||
export function makeNotice(body: string): RoomMessageEventContent {
|
||||
export function makeNotice(body: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Notice,
|
||||
body: body,
|
||||
@@ -106,7 +106,7 @@ export function makeNotice(body: string): RoomMessageEventContent {
|
||||
* @param body - the plaintext body of the emote
|
||||
* @returns
|
||||
*/
|
||||
export function makeEmoteMessage(body: string): RoomMessageEventContent {
|
||||
export function makeEmoteMessage(body: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Emote,
|
||||
body: body,
|
||||
@@ -185,37 +185,27 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
|
||||
/**
|
||||
* Topic event helpers
|
||||
*/
|
||||
export type MakeTopicContent = (topic: string | null | undefined, htmlTopic?: string) => MRoomTopicEventContent;
|
||||
export type MakeTopicContent = (topic: string, htmlTopic?: string) => MRoomTopicEventContent;
|
||||
|
||||
export const makeTopicContent: MakeTopicContent = (topic, htmlTopic) => {
|
||||
const renderings = [];
|
||||
// Put HTML first because clients will render the first type in
|
||||
// the array that they understand
|
||||
const renderings = [{ body: topic, mimetype: "text/plain" }];
|
||||
if (isProvided(htmlTopic)) {
|
||||
renderings.push({ body: htmlTopic, mimetype: "text/html" });
|
||||
renderings.push({ body: htmlTopic!, mimetype: "text/html" });
|
||||
}
|
||||
if (isProvided(topic)) {
|
||||
renderings.push({ body: topic, mimetype: "text/plain" });
|
||||
}
|
||||
return { topic, [M_TOPIC.name]: { "m.text": renderings } };
|
||||
return { topic, [M_TOPIC.name]: renderings };
|
||||
};
|
||||
|
||||
export type TopicState = {
|
||||
text?: string;
|
||||
text: string;
|
||||
html?: string;
|
||||
};
|
||||
|
||||
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
|
||||
const mtopicParent = M_TOPIC.findIn<MTopicContent | IMessageRendering[]>(content as MTopicEvent);
|
||||
const mtopic = Array.isArray(mtopicParent) ? mtopicParent : mtopicParent?.["m.text"];
|
||||
// TODO remove support for the old malformed m.topic arrays after a few releases (only allow array in m.text)
|
||||
// https://github.com/matrix-org/matrix-js-sdk/pull/4984#pullrequestreview-3174251065
|
||||
//const mtopic = M_TOPIC.findIn<MTopicContent>(content)?.["m.text"];
|
||||
const mtopic = M_TOPIC.findIn<MTopicContent>(content);
|
||||
if (!Array.isArray(mtopic)) {
|
||||
return { text: content.topic ?? undefined };
|
||||
return { text: content.topic };
|
||||
}
|
||||
const text =
|
||||
mtopic?.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic ?? undefined;
|
||||
const text = mtopic?.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic;
|
||||
const html = mtopic?.find((r) => r.mimetype === "text/html")?.body;
|
||||
return { text, html };
|
||||
};
|
||||
|
||||
85
node_modules/matrix-js-sdk/src/content-repo.ts
generated
vendored
85
node_modules/matrix-js-sdk/src/content-repo.ts
generated
vendored
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015 - 2024 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2021 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.
|
||||
@@ -14,22 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Validation based on https://spec.matrix.org/v1.12/appendices/#server-name
|
||||
// We do not use the validation described in https://spec.matrix.org/v1.12/client-server-api/#security-considerations-5
|
||||
// as it'd wrongly make all MXCs invalid due to not allowing `[].:` in server names.
|
||||
const serverNameRegex =
|
||||
/^(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(?:\[[\dA-Fa-f:.]{2,45}])|(?:[A-Za-z\d\-.]{1,255}))(?::\d{1,5})?$/;
|
||||
function validateServerName(serverName: string): boolean {
|
||||
const matches = serverNameRegex.exec(serverName);
|
||||
return matches?.[0] === serverName;
|
||||
}
|
||||
|
||||
// Validation based on https://spec.matrix.org/v1.12/client-server-api/#security-considerations-5
|
||||
const mediaIdRegex = /^[\w-]+$/;
|
||||
function validateMediaId(mediaId: string): boolean {
|
||||
const matches = mediaIdRegex.exec(mediaId);
|
||||
return matches?.[0] === mediaId;
|
||||
}
|
||||
import { encodeParams } from "./utils";
|
||||
|
||||
/**
|
||||
* Get the HTTP URL for an MXC URI.
|
||||
@@ -43,16 +28,7 @@ function validateMediaId(mediaId: string): boolean {
|
||||
* directly. Fetching such URLs will leak information about the user to
|
||||
* anyone they share a room with. If false, will return the emptry string
|
||||
* for such URLs.
|
||||
* @param allowRedirects - If true, the caller supports the URL being 307 or
|
||||
* 308 redirected to another resource upon request. If false, redirects
|
||||
* are not expected. Implied `true` when `useAuthentication` is `true`.
|
||||
* @param useAuthentication - If true, the caller supports authenticated
|
||||
* media and wants an authentication-required URL. Note that server support
|
||||
* for authenticated media will *not* be checked - it is the caller's responsibility
|
||||
* to do so before calling this function. Note also that `useAuthentication`
|
||||
* implies `allowRedirects`. Defaults to false (unauthenticated endpoints).
|
||||
* @param animated - Whether the desired thumbnail should be animated.
|
||||
* @returns The complete URL to the content, may be an empty string if the provided mxc is not valid.
|
||||
* @returns The complete URL to the content.
|
||||
*/
|
||||
export function getHttpUriForMxc(
|
||||
baseUrl: string,
|
||||
@@ -61,62 +37,43 @@ export function getHttpUriForMxc(
|
||||
height?: number,
|
||||
resizeMethod?: string,
|
||||
allowDirectLinks = false,
|
||||
allowRedirects?: boolean,
|
||||
useAuthentication?: boolean,
|
||||
animated?: boolean,
|
||||
): string {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
return "";
|
||||
}
|
||||
if (!mxc.startsWith("mxc://")) {
|
||||
if (mxc.indexOf("mxc://") !== 0) {
|
||||
if (allowDirectLinks) {
|
||||
return mxc;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const [serverName, mediaId, ...rest] = mxc.slice(6).split("/");
|
||||
if (rest.length > 0 || !validateServerName(serverName) || !validateMediaId(mediaId)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (useAuthentication) {
|
||||
allowRedirects = true; // per docs (MSC3916 always expects redirects)
|
||||
|
||||
// Dev note: MSC3916 removes `allow_redirect` entirely, but
|
||||
// for explicitness we set it here. This makes it slightly more obvious to
|
||||
// callers, hopefully.
|
||||
}
|
||||
|
||||
let prefix: string;
|
||||
const isThumbnailRequest = !!width || !!height || !!resizeMethod;
|
||||
const verb = isThumbnailRequest ? "thumbnail" : "download";
|
||||
if (useAuthentication) {
|
||||
prefix = `/_matrix/client/v1/media/${verb}`;
|
||||
} else {
|
||||
prefix = `/_matrix/media/v3/${verb}`;
|
||||
}
|
||||
|
||||
const url = new URL(`${prefix}/${serverName}/${mediaId}`, baseUrl);
|
||||
let serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
let prefix = "/_matrix/media/r0/download/";
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (width) {
|
||||
url.searchParams.set("width", Math.round(width).toString());
|
||||
params["width"] = Math.round(width).toString();
|
||||
}
|
||||
if (height) {
|
||||
url.searchParams.set("height", Math.round(height).toString());
|
||||
params["height"] = Math.round(height).toString();
|
||||
}
|
||||
if (resizeMethod) {
|
||||
url.searchParams.set("method", resizeMethod);
|
||||
params["method"] = resizeMethod;
|
||||
}
|
||||
if (animated !== undefined) {
|
||||
url.searchParams.set("animated", String(animated));
|
||||
if (Object.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/r0/thumbnail/";
|
||||
}
|
||||
|
||||
if (typeof allowRedirects === "boolean") {
|
||||
// We add this after, so we don't convert everything to a thumbnail request.
|
||||
url.searchParams.set("allow_redirect", JSON.stringify(allowRedirects));
|
||||
const fragmentOffset = serverAndMediaId.indexOf("#");
|
||||
let fragment = "";
|
||||
if (fragmentOffset >= 0) {
|
||||
fragment = serverAndMediaId.slice(fragmentOffset);
|
||||
serverAndMediaId = serverAndMediaId.slice(0, fragmentOffset);
|
||||
}
|
||||
|
||||
return url.href;
|
||||
const urlParams = Object.keys(params).length === 0 ? "" : "?" + encodeParams(params);
|
||||
return baseUrl + prefix + serverAndMediaId + urlParams + fragment;
|
||||
}
|
||||
|
||||
152
node_modules/matrix-js-sdk/src/crypto-api/CryptoEvent.ts
generated
vendored
152
node_modules/matrix-js-sdk/src/crypto-api/CryptoEvent.ts
generated
vendored
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cryptography-related events emitted by the {@link matrix.MatrixClient}.
|
||||
*/
|
||||
export enum CryptoEvent {
|
||||
/**
|
||||
* Fires when the trust status of a user changes.
|
||||
* The payload is a pair (userId, userTrustLevel). The trust level is one of the values from UserVerificationStatus.
|
||||
*/
|
||||
UserTrustStatusChanged = "userTrustStatusChanged",
|
||||
|
||||
/**
|
||||
* Fires when the key backup status changes.
|
||||
* The payload is a boolean indicating whether the key backup is enabled.
|
||||
*/
|
||||
KeyBackupStatus = "crypto.keyBackupStatus",
|
||||
|
||||
/**
|
||||
* Fires when we failed to back up the keys
|
||||
* The payload is the error code of the error that occurred.
|
||||
*/
|
||||
KeyBackupFailed = "crypto.keyBackupFailed",
|
||||
|
||||
/**
|
||||
* Fires when the number of sessions that can be backed up changes.
|
||||
* The payload is the remaining number of sessions that can be backed up.
|
||||
*/
|
||||
KeyBackupSessionsRemaining = "crypto.keyBackupSessionsRemaining",
|
||||
|
||||
/**
|
||||
* Fires when a new valid backup decryption key is in cache.
|
||||
* This will happen when a secret is received from another session, from secret storage,
|
||||
* or when a new backup is created from this session.
|
||||
*
|
||||
* The payload is the version of the backup for which we have the key for.
|
||||
*
|
||||
* This event is only fired by the rust crypto backend.
|
||||
*/
|
||||
KeyBackupDecryptionKeyCached = "crypto.keyBackupDecryptionKeyCached",
|
||||
|
||||
/**
|
||||
* Fires when a key verification request is received.
|
||||
* The payload is a VerificationRequest object representing the request.
|
||||
*/
|
||||
VerificationRequestReceived = "crypto.verificationRequestReceived",
|
||||
|
||||
/** @deprecated Use {@link DevicesUpdated} instead when using rust crypto */
|
||||
WillUpdateDevices = "crypto.willUpdateDevices",
|
||||
|
||||
/**
|
||||
* Fires whenever the stored devices for a user have been updated
|
||||
* The payload is a pair (userIds, initialFetch).
|
||||
*/
|
||||
DevicesUpdated = "crypto.devicesUpdated",
|
||||
|
||||
/**
|
||||
* Fires when the user's cross-signing keys have changed or cross-signing
|
||||
* has been enabled/disabled. The client can use getStoredCrossSigningForUser
|
||||
* with the user ID of the logged in user to check if cross-signing is
|
||||
* enabled on the account. If enabled, it can test whether the current key
|
||||
* is trusted using with checkUserTrust with the user ID of the logged
|
||||
* in user. The checkOwnCrossSigningTrust function may be used to reconcile
|
||||
* the trust in the account key.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
* @experimental
|
||||
*/
|
||||
KeysChanged = "crossSigning.keysChanged",
|
||||
|
||||
/**
|
||||
* Fires when data is being migrated from legacy crypto to rust crypto.
|
||||
*
|
||||
* The payload is a pair `(progress, total)`, where `progress` is the number of steps completed so far, and
|
||||
* `total` is the total number of steps. When migration is complete, a final instance of the event is emitted, with
|
||||
* `progress === total === -1`.
|
||||
*/
|
||||
LegacyCryptoStoreMigrationProgress = "crypto.legacyCryptoStoreMigrationProgress",
|
||||
|
||||
/**
|
||||
* Fires when a new dehydrated device is created locally.
|
||||
*
|
||||
* After the client calls {@link CryptoApi.startDehydration}, this event
|
||||
* will be fired every time a new dehydrated device is created. It may fire
|
||||
* before `startDehydration` returns.
|
||||
*/
|
||||
DehydratedDeviceCreated = "dehydration.DehydratedDeviceCreated",
|
||||
|
||||
/**
|
||||
* Fires when a new dehydrated device is successfully uploaded to the server.
|
||||
*
|
||||
* This should fire shortly after {@link DehydratedDeviceCreated} fires. If
|
||||
* upload is unsuccessful, this will be reported either by an error thrown
|
||||
* by {@link CryptoApi.startDehydration} (for errors that happen before
|
||||
* `startDehydration` returns), or by firing {@link DehydratedDeviceRotationError}
|
||||
* (for errors that happen during regular rotation of the dehydrated device)
|
||||
*/
|
||||
DehydratedDeviceUploaded = "dehydration.DehydratedDeviceUploaded",
|
||||
|
||||
/**
|
||||
* Fires when rehydration has started.
|
||||
*
|
||||
* After the client calls {@link CryptoApi.startDehydration}, this event will
|
||||
* fire if a dehydrated device is found and we attempt to rehydrate it.
|
||||
*/
|
||||
RehydrationStarted = "dehydration.RehydrationStarted",
|
||||
|
||||
/**
|
||||
* Fires during rehydration, to inform the application of rehydration progress.
|
||||
*
|
||||
* The payload is a pair `[roomKeyCount: number, toDeviceCount: number]`,
|
||||
* where `roomKeyCount` is the number of room keys that have been received
|
||||
* so far, and `toDeviceCount` is the number of to-device messages received
|
||||
* so far (including the messages containing room keys).
|
||||
*/
|
||||
RehydrationProgress = "dehydration.RehydrationProgress",
|
||||
|
||||
/** Fires when rehydration has completed successfully. */
|
||||
RehydrationCompleted = "dehydration.RehydrationCompleted",
|
||||
|
||||
/** Fires when there was an error in rehydration.
|
||||
*
|
||||
* The payload is an error message as a string.
|
||||
*/
|
||||
RehydrationError = "dehydration.RehydrationError",
|
||||
|
||||
/**
|
||||
* Fires when a dehydrated device key has been cached in the local database.
|
||||
*/
|
||||
DehydrationKeyCached = "dehydration.DehydrationKeyCached",
|
||||
|
||||
/**
|
||||
* Fires when an error occurs during periodic rotation of the dehydrated device.
|
||||
*
|
||||
* The payload is an error message as a string.
|
||||
*/
|
||||
DehydratedDeviceRotationError = "dehydration.DehydratedDeviceRotationError",
|
||||
}
|
||||
42
node_modules/matrix-js-sdk/src/crypto-api/CryptoEventHandlerMap.ts
generated
vendored
42
node_modules/matrix-js-sdk/src/crypto-api/CryptoEventHandlerMap.ts
generated
vendored
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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 { type CryptoEvent } from "./CryptoEvent.ts";
|
||||
import { type VerificationRequest } from "./verification.ts";
|
||||
import { type UserVerificationStatus } from "./index.ts";
|
||||
import { type RustBackupCryptoEventMap } from "../rust-crypto/backup.ts";
|
||||
import { type EmptyObject } from "../@types/common.ts";
|
||||
|
||||
/**
|
||||
* A map of the {@link CryptoEvent} fired by the {@link CryptoApi} and their payloads.
|
||||
*/
|
||||
export type CryptoEventHandlerMap = {
|
||||
[CryptoEvent.VerificationRequestReceived]: (request: VerificationRequest) => void;
|
||||
[CryptoEvent.UserTrustStatusChanged]: (userId: string, userTrustLevel: UserVerificationStatus) => void;
|
||||
[CryptoEvent.KeyBackupDecryptionKeyCached]: (version: string) => void;
|
||||
[CryptoEvent.KeysChanged]: (data: EmptyObject) => void;
|
||||
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void;
|
||||
[CryptoEvent.DehydratedDeviceCreated]: () => void;
|
||||
[CryptoEvent.DehydratedDeviceUploaded]: () => void;
|
||||
[CryptoEvent.RehydrationStarted]: () => void;
|
||||
[CryptoEvent.RehydrationProgress]: (roomKeyCount: number, toDeviceCount: number) => void;
|
||||
[CryptoEvent.RehydrationCompleted]: () => void;
|
||||
[CryptoEvent.RehydrationError]: (msg: string) => void;
|
||||
[CryptoEvent.DehydrationKeyCached]: () => void;
|
||||
[CryptoEvent.DehydratedDeviceRotationError]: (msg: string) => void;
|
||||
} & RustBackupCryptoEventMap;
|
||||
1390
node_modules/matrix-js-sdk/src/crypto-api/index.ts
generated
vendored
1390
node_modules/matrix-js-sdk/src/crypto-api/index.ts
generated
vendored
File diff suppressed because it is too large
Load Diff
58
node_modules/matrix-js-sdk/src/crypto-api/key-passphrase.ts
generated
vendored
58
node_modules/matrix-js-sdk/src/crypto-api/key-passphrase.ts
generated
vendored
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
*/
|
||||
|
||||
const DEFAULT_BIT_SIZE = 256;
|
||||
|
||||
/**
|
||||
* Derive a recovery key from a passphrase and salt using PBKDF2.
|
||||
* @see https://spec.matrix.org/v1.11/client-server-api/#deriving-keys-from-passphrases
|
||||
*
|
||||
* @param passphrase - The passphrase to derive the key from
|
||||
* @param salt - The salt to use in the derivation
|
||||
* @param iterations - The number of iterations to use in the derivation
|
||||
* @param numBits - The number of bits to derive
|
||||
*/
|
||||
export async function deriveRecoveryKeyFromPassphrase(
|
||||
passphrase: string,
|
||||
salt: string,
|
||||
iterations: number,
|
||||
numBits = DEFAULT_BIT_SIZE,
|
||||
): Promise<Uint8Array<ArrayBuffer>> {
|
||||
if (!globalThis.crypto.subtle || !TextEncoder) {
|
||||
throw new Error("Password-based backup is not available on this platform");
|
||||
}
|
||||
|
||||
const key = await globalThis.crypto.subtle.importKey(
|
||||
"raw",
|
||||
new TextEncoder().encode(passphrase),
|
||||
{ name: "PBKDF2" },
|
||||
false,
|
||||
["deriveBits"],
|
||||
);
|
||||
|
||||
const keybits = await globalThis.crypto.subtle.deriveBits(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: new TextEncoder().encode(salt),
|
||||
iterations: iterations,
|
||||
hash: "SHA-512",
|
||||
},
|
||||
key,
|
||||
numBits,
|
||||
);
|
||||
|
||||
return new Uint8Array(keybits);
|
||||
}
|
||||
91
node_modules/matrix-js-sdk/src/crypto-api/keybackup.ts
generated
vendored
91
node_modules/matrix-js-sdk/src/crypto-api/keybackup.ts
generated
vendored
@@ -14,9 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ISigned } from "../@types/signed.ts";
|
||||
import { type AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||
import { type ImportRoomKeyProgressData } from "./index.ts";
|
||||
import { ISigned } from "../@types/signed";
|
||||
|
||||
export interface Curve25519AuthData {
|
||||
public_key: string;
|
||||
@@ -33,89 +31,12 @@ export interface Aes256AuthData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about a new server-side key backup.
|
||||
*
|
||||
* The type of the request body for [`POST /_matrix/client/v3/room_keys/version`](https://spec.matrix.org/v1.19/client-server-api/#post_matrixclientv3room_keysversion).
|
||||
* Extra info of a recovery key
|
||||
*/
|
||||
export interface NewKeyBackupInfo {
|
||||
export interface KeyBackupInfo {
|
||||
algorithm: string;
|
||||
auth_data: ISigned & (Curve25519AuthData | Aes256AuthData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about an existing server-side key backup.
|
||||
*
|
||||
* Returned by [`GET /_matrix/client/v3/room_keys/version`](https://spec.matrix.org/v1.7/client-server-api/#get_matrixclientv3room_keysversion).
|
||||
*/
|
||||
export interface KeyBackupInfo extends NewKeyBackupInfo {
|
||||
count: number;
|
||||
etag: string;
|
||||
version: string; // number contained within
|
||||
}
|
||||
|
||||
/**
|
||||
* Information on whether a given server-side backup is trusted.
|
||||
*/
|
||||
export interface BackupTrustInfo {
|
||||
/**
|
||||
* Is this backup trusted?
|
||||
*
|
||||
* True if, and only if, there is a valid signature on the backup from a trusted device.
|
||||
*/
|
||||
readonly trusted: boolean;
|
||||
|
||||
/**
|
||||
* True if this backup matches the stored decryption key.
|
||||
*/
|
||||
readonly matchesDecryptionKey: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of {@link CryptoApi.checkKeyBackupAndEnable}.
|
||||
*/
|
||||
export interface KeyBackupCheck {
|
||||
backupInfo: KeyBackupInfo;
|
||||
trustInfo: BackupTrustInfo;
|
||||
}
|
||||
|
||||
export interface Curve25519SessionData {
|
||||
ciphertext: string;
|
||||
ephemeral: string;
|
||||
mac: string;
|
||||
}
|
||||
|
||||
export interface KeyBackupSession<T = Curve25519SessionData | AESEncryptedSecretStoragePayload> {
|
||||
first_message_index: number;
|
||||
forwarded_count: number;
|
||||
is_verified: boolean;
|
||||
session_data: T;
|
||||
}
|
||||
|
||||
export interface KeyBackupRoomSessions {
|
||||
[sessionId: string]: KeyBackupSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra parameters for {@link CryptoApi.restoreKeyBackup} and {@link CryptoApi.restoreKeyBackupWithPassphrase}.
|
||||
*/
|
||||
export interface KeyBackupRestoreOpts {
|
||||
/**
|
||||
* A callback which, if defined, will be called periodically to report ongoing progress of the backup restore process.
|
||||
* @param progress
|
||||
*/
|
||||
progressCallback?: (progress: ImportRoomKeyProgressData) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of {@link CryptoApi.restoreKeyBackup}.
|
||||
*/
|
||||
export interface KeyBackupRestoreResult {
|
||||
/**
|
||||
* The total number of keys that were found in the backup.
|
||||
*/
|
||||
total: number;
|
||||
/**
|
||||
* The number of keys that were imported.
|
||||
*/
|
||||
imported: number;
|
||||
count?: number;
|
||||
etag?: string;
|
||||
version?: string; // number contained within
|
||||
}
|
||||
|
||||
69
node_modules/matrix-js-sdk/src/crypto-api/recovery-key.ts
generated
vendored
69
node_modules/matrix-js-sdk/src/crypto-api/recovery-key.ts
generated
vendored
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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 bs58 from "bs58";
|
||||
|
||||
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
|
||||
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
|
||||
const OLM_RECOVERY_KEY_PREFIX = [0x8b, 0x01];
|
||||
const KEY_SIZE = 32;
|
||||
|
||||
/**
|
||||
* Encode a recovery key using the Matrix {@link https://spec.matrix.org/v1.11/appendices/#cryptographic-key-representation | Cryptographic key representation}
|
||||
* @param key
|
||||
*/
|
||||
export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
|
||||
const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
|
||||
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
|
||||
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
|
||||
|
||||
let parity = 0;
|
||||
for (let i = 0; i < buf.length - 1; ++i) {
|
||||
parity ^= buf[i];
|
||||
}
|
||||
buf[buf.length - 1] = parity;
|
||||
const base58key = bs58.encode(buf);
|
||||
|
||||
return base58key.match(/.{1,4}/g)?.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a recovery key encoded with the Matrix {@link https://spec.matrix.org/v1.11/appendices/#cryptographic-key-representation | Cryptographic key representation} encoding.
|
||||
* @param recoveryKey
|
||||
*/
|
||||
export function decodeRecoveryKey(recoveryKey: string): Uint8Array<ArrayBuffer> {
|
||||
const result = bs58.decode(recoveryKey.replace(/ /g, ""));
|
||||
|
||||
let parity = 0;
|
||||
for (const b of result) {
|
||||
parity ^= b;
|
||||
}
|
||||
if (parity !== 0) {
|
||||
throw new Error("Incorrect parity");
|
||||
}
|
||||
|
||||
for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
|
||||
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
|
||||
throw new Error("Incorrect prefix");
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE + 1) {
|
||||
throw new Error("Incorrect length");
|
||||
}
|
||||
|
||||
return Uint8Array.from(result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE));
|
||||
}
|
||||
90
node_modules/matrix-js-sdk/src/crypto-api/verification.ts
generated
vendored
90
node_modules/matrix-js-sdk/src/crypto-api/verification.ts
generated
vendored
@@ -14,16 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MatrixEvent } from "../models/event.ts";
|
||||
import { type TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
/**
|
||||
* An incoming, or outgoing, request to verify a user or a device via cross-signing.
|
||||
*/
|
||||
export interface VerificationRequest extends TypedEventEmitter<
|
||||
VerificationRequestEvent,
|
||||
VerificationRequestEventHandlerMap
|
||||
> {
|
||||
export interface VerificationRequest
|
||||
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap> {
|
||||
/**
|
||||
* Unique ID for this verification request.
|
||||
*
|
||||
@@ -116,18 +114,29 @@ export interface VerificationRequest extends TypedEventEmitter<
|
||||
*/
|
||||
cancel(params?: { reason?: string; code?: string }): Promise<void>;
|
||||
|
||||
/**
|
||||
* Create a {@link Verifier} to do this verification via a particular method.
|
||||
*
|
||||
* If a verifier has already been created for this request, returns that verifier.
|
||||
*
|
||||
* This does *not* send the `m.key.verification.start` event - to do so, call {@link Crypto.Verifier#verify} on the
|
||||
* returned verifier.
|
||||
*
|
||||
* If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
|
||||
*
|
||||
* @param method - the name of the verification method to use.
|
||||
* @param targetDevice - details of where to send the request to.
|
||||
*
|
||||
* @returns The verifier which will do the actual verification.
|
||||
*
|
||||
* @deprecated Use {@link VerificationRequest#startVerification} instead.
|
||||
*/
|
||||
beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier;
|
||||
|
||||
/**
|
||||
* Send an `m.key.verification.start` event to start verification via a particular method.
|
||||
*
|
||||
* This is used for SAS (emoji) verification: `method` should be set to `m.sas.v1`. It does not start
|
||||
* QR-code verification, and passing a QR-code method (such as `m.reciprocate.v1`, `m.qr_code.scan.v1`,
|
||||
* or `m.qr_code.show.v1`) will be rejected with an "Unsupported verification method" error.
|
||||
*
|
||||
* For QR-code verification, use {@link VerificationRequest#generateQRCode} to display a QR code to the
|
||||
* other device, or {@link VerificationRequest#scanQRCode} to consume a QR code scanned from it. This is
|
||||
* only possible once the `phase` is {@link VerificationPhase.Ready}; when it is, a client can typically
|
||||
* offer the user any of three options: show a QR code, scan the other party's QR code, or fall back to
|
||||
* emoji (SAS) verification via this method.
|
||||
* This is normally used when starting a verification via emojis (ie, `method` is set to `m.sas.v1`).
|
||||
*
|
||||
* @param method - the name of the verification method to use.
|
||||
*
|
||||
@@ -138,17 +147,15 @@ export interface VerificationRequest extends TypedEventEmitter<
|
||||
/**
|
||||
* Start a QR code verification by providing a scanned QR code for this verification flow.
|
||||
*
|
||||
* Call this once the user has scanned the QR code displayed by the other device (for example, the bytes
|
||||
* produced by that device's {@link VerificationRequest#generateQRCode}). Validates the QR code, and if it
|
||||
* is ok, sends an `m.key.verification.start` event with `method` set to `m.reciprocate.v1`, to tell the
|
||||
* other side the scan was successful.
|
||||
* Validates the QR code, and if it is ok, sends an `m.key.verification.start` event with `method` set to
|
||||
* `m.reciprocate.v1`, to tell the other side the scan was successful.
|
||||
*
|
||||
* See also {@link VerificationRequest#startVerification} which can be used to start other verification methods.
|
||||
*
|
||||
* @param qrCodeData - the decoded QR code.
|
||||
* @returns A verifier; call `.verify()` on it to wait for the other side to complete the verification flow.
|
||||
*/
|
||||
scanQRCode(qrCodeData: Uint8ClampedArray): Promise<Verifier>;
|
||||
scanQRCode(qrCodeData: Uint8Array): Promise<Verifier>;
|
||||
|
||||
/**
|
||||
* The verifier which is doing the actual verification, once the method has been established.
|
||||
@@ -156,21 +163,22 @@ export interface VerificationRequest extends TypedEventEmitter<
|
||||
*/
|
||||
get verifier(): Verifier | undefined;
|
||||
|
||||
/**
|
||||
* Get the data for a QR code allowing the other device to verify this one, if it supports it.
|
||||
*
|
||||
* Only set after a .ready if the other party can scan a QR code, otherwise undefined.
|
||||
*
|
||||
* @deprecated Not supported in Rust Crypto. Use {@link VerificationRequest#generateQRCode} instead.
|
||||
*/
|
||||
getQRCodeBytes(): Buffer | undefined;
|
||||
|
||||
/**
|
||||
* Generate the data for a QR code allowing the other device to verify this one, if it supports it.
|
||||
*
|
||||
* Returns the QR code data only when all of the following hold; otherwise it returns `undefined`:
|
||||
* - `phase` is {@link VerificationPhase.Ready};
|
||||
* - the other party advertises support for scanning a QR code (ie, `otherPartySupportsMethod("m.qr_code.scan.v1")`
|
||||
* is true); and
|
||||
* - this device has its cross-signing keys available locally. A frequent cause of an unexpected `undefined`
|
||||
* is that cross-signing has not been set up or the keys have not yet been fetched, so the QR code cannot be
|
||||
* constructed even though the phase and method checks pass.
|
||||
*
|
||||
* On success, display the returned bytes as a QR code for the other device to scan; if the other side scans it
|
||||
* and confirms, there is nothing further to do on this side.
|
||||
* Only returns data once `phase` is {@link VerificationPhase.Ready} and the other party can scan a QR code;
|
||||
* otherwise returns `undefined`.
|
||||
*/
|
||||
generateQRCode(): Promise<Uint8ClampedArray | undefined>;
|
||||
generateQRCode(): Promise<Buffer | undefined>;
|
||||
|
||||
/**
|
||||
* If this request has been cancelled, the cancellation code (e.g `m.user`) which is responsible for cancelling
|
||||
@@ -216,25 +224,13 @@ export enum VerificationPhase {
|
||||
/** An `m.key.verification.ready` event has been sent or received, indicating the verification request is accepted. */
|
||||
Ready,
|
||||
|
||||
/**
|
||||
* The verification is in flight.
|
||||
*
|
||||
* This means that an `m.key.verification.start` event has been sent or received, choosing a verification method;
|
||||
* however the verification has not yet completed or been cancelled.
|
||||
*/
|
||||
/** An `m.key.verification.start` event has been sent or received, choosing a verification method */
|
||||
Started,
|
||||
|
||||
/**
|
||||
* An `m.key.verification.cancel` event has been sent or received at any time before the `done` event, cancelling
|
||||
* the verification request
|
||||
*/
|
||||
/** An `m.key.verification.cancel` event has been sent or received at any time before the `done` event, cancelling the verification request */
|
||||
Cancelled,
|
||||
|
||||
/**
|
||||
* The verification request is complete.
|
||||
*
|
||||
* Normally this means that `m.key.verification.done` events have been sent and received.
|
||||
*/
|
||||
/** An `m.key.verification.done` event has been **sent**, completing the verification request. */
|
||||
Done,
|
||||
}
|
||||
|
||||
@@ -273,7 +269,7 @@ export interface Verifier extends TypedEventEmitter<VerifierEvent, VerifierEvent
|
||||
* Cancel a verification.
|
||||
*
|
||||
* We will send an `m.key.verification.cancel` if the verification is still in flight. The verification promise
|
||||
* will reject, and a {@link crypto-api.VerifierEvent.Cancel | VerifierEvent.Cancel} will be emitted.
|
||||
* will reject, and a {@link Crypto.VerifierEvent#Cancel} will be emitted.
|
||||
*
|
||||
* @param e - the reason for the cancellation.
|
||||
*/
|
||||
|
||||
317
node_modules/matrix-js-sdk/src/crypto/store/base.ts
generated
vendored
317
node_modules/matrix-js-sdk/src/crypto/store/base.ts
generated
vendored
@@ -14,67 +14,66 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Logger } from "../../logger.ts";
|
||||
import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
import { type AESEncryptedSecretStoragePayload } from "../../@types/AESEncryptedSecretStoragePayload.ts";
|
||||
import { type ISignatures } from "../../@types/signed.ts";
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { TrackingStatus } from "../DeviceList";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { IDevice } from "../deviceinfo";
|
||||
import { ICrossSigningInfo } from "../CrossSigning";
|
||||
import { PrefixedLogger } from "../../logger";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
import { DehydrationManager } from "../dehydration";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
/**
|
||||
* Internal module. Definitions for storage for the crypto module
|
||||
*/
|
||||
|
||||
export interface SecretStorePrivateKeys {
|
||||
"m.megolm_backup.v1": AESEncryptedSecretStoragePayload;
|
||||
"dehydration": {
|
||||
keyInfo: DehydrationManager["keyInfo"];
|
||||
key: IEncryptedPayload;
|
||||
deviceDisplayName: string;
|
||||
time: number;
|
||||
} | null;
|
||||
"m.megolm_backup.v1": IEncryptedPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstraction of things that can store data required for end-to-end encryption
|
||||
*/
|
||||
export interface CryptoStore {
|
||||
/**
|
||||
* Returns true if this CryptoStore has ever been initialised (ie, it might contain data).
|
||||
*
|
||||
* Unlike the rest of the methods in this interface, can be called before {@link CryptoStore#startup}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
containsData(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Initialise this crypto store.
|
||||
*
|
||||
* Typically, this involves provisioning storage, and migrating any existing data to the current version of the
|
||||
* storage schema where appropriate.
|
||||
*
|
||||
* Must be called before any of the rest of the methods in this interface.
|
||||
*/
|
||||
startup(): Promise<CryptoStore>;
|
||||
|
||||
deleteAllData(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get data on how much of the libolm to Rust Crypto migration has been done.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
getMigrationState(): Promise<MigrationState>;
|
||||
|
||||
/**
|
||||
* Set data on how much of the libolm to Rust Crypto migration has been done.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
setMigrationState(migrationState: MigrationState): Promise<void>;
|
||||
getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest>;
|
||||
getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null>;
|
||||
getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null>;
|
||||
getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]>;
|
||||
getOutgoingRoomKeyRequestsByTarget(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]>;
|
||||
updateOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
updates: Partial<OutgoingRoomKeyRequest>,
|
||||
): Promise<OutgoingRoomKeyRequest | null>;
|
||||
deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise<OutgoingRoomKeyRequest | null>;
|
||||
|
||||
// Olm Account
|
||||
getAccount(txn: unknown, func: (accountPickle: string | null) => void): void;
|
||||
storeAccount(txn: unknown, accountPickle: string): void;
|
||||
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void;
|
||||
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void;
|
||||
getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: unknown,
|
||||
func: (key: SecretStorePrivateKeys[K] | null) => void,
|
||||
type: K,
|
||||
): void;
|
||||
storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void;
|
||||
storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: unknown,
|
||||
type: K,
|
||||
@@ -94,25 +93,11 @@ export interface CryptoStore {
|
||||
txn: unknown,
|
||||
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
|
||||
): void;
|
||||
|
||||
getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo | null) => void): void;
|
||||
storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void;
|
||||
|
||||
/**
|
||||
* Get a batch of end-to-end sessions from the database.
|
||||
*
|
||||
* @returns A batch of Olm Sessions, or `null` if no sessions are left.
|
||||
* @internal
|
||||
*/
|
||||
getEndToEndSessionsBatch(): Promise<ISessionInfo[] | null>;
|
||||
|
||||
/**
|
||||
* Delete a batch of end-to-end sessions from the database.
|
||||
*
|
||||
* Any sessions in the list which are not found are silently ignored.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
deleteEndToEndSessionsBatch(sessions: { deviceKey?: string; sessionId?: string }[]): Promise<void>;
|
||||
storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void>;
|
||||
getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null>;
|
||||
filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]>;
|
||||
|
||||
// Inbound Group Sessions
|
||||
getEndToEndInboundGroupSession(
|
||||
@@ -121,60 +106,55 @@ export interface CryptoStore {
|
||||
txn: unknown,
|
||||
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
|
||||
): void;
|
||||
getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void;
|
||||
addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: unknown,
|
||||
): void;
|
||||
storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: unknown,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Count the number of Megolm sessions in the database.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
countEndToEndInboundGroupSessions(): Promise<number>;
|
||||
|
||||
/**
|
||||
* Get a batch of Megolm sessions from the database.
|
||||
*
|
||||
* @returns A batch of Megolm Sessions, or `null` if no sessions are left.
|
||||
* @internal
|
||||
*/
|
||||
getEndToEndInboundGroupSessionsBatch(): Promise<SessionExtended[] | null>;
|
||||
|
||||
/**
|
||||
* Delete a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Any sessions in the list which are not found are silently ignored.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
deleteEndToEndInboundGroupSessionsBatch(sessions: { senderKey: string; sessionId: string }[]): Promise<void>;
|
||||
storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: IWithheld,
|
||||
txn: unknown,
|
||||
): void;
|
||||
|
||||
// Device Data
|
||||
getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void;
|
||||
storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void;
|
||||
storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void;
|
||||
getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void;
|
||||
getSessionsNeedingBackup(limit: number): Promise<ISession[]>;
|
||||
countSessionsNeedingBackup(txn?: unknown): Promise<number>;
|
||||
unmarkSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
|
||||
markSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
|
||||
addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string, txn?: unknown): void;
|
||||
getSharedHistoryInboundGroupSessions(
|
||||
roomId: string,
|
||||
txn?: unknown,
|
||||
): Promise<[senderKey: string, sessionId: string][]>;
|
||||
addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
|
||||
takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
|
||||
|
||||
// Session key backups
|
||||
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: Logger): Promise<T>;
|
||||
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
|
||||
}
|
||||
|
||||
export type Mode = "readonly" | "readwrite";
|
||||
|
||||
/** Data on a Megolm session */
|
||||
export interface ISession {
|
||||
senderKey: string;
|
||||
sessionId: string;
|
||||
sessionData?: InboundGroupSessionData;
|
||||
}
|
||||
|
||||
/** Extended data on a Megolm session */
|
||||
export interface SessionExtended extends ISession {
|
||||
needsBackup: boolean;
|
||||
}
|
||||
|
||||
/** Data on an Olm session */
|
||||
export interface ISessionInfo {
|
||||
deviceKey?: string;
|
||||
sessionId?: string;
|
||||
@@ -195,7 +175,14 @@ export interface IDeviceData {
|
||||
syncToken?: string;
|
||||
}
|
||||
|
||||
export interface IProblem {
|
||||
type: string;
|
||||
fixed: boolean;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface IWithheld {
|
||||
// eslint-disable-next-line camelcase
|
||||
room_id: string;
|
||||
code: string;
|
||||
reason: string;
|
||||
@@ -229,153 +216,11 @@ export interface OutgoingRoomKeyRequest {
|
||||
state: RoomKeyRequestState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keys for the `account` object store to store the migration state.
|
||||
* Values are defined in `MigrationState`.
|
||||
* @internal
|
||||
*/
|
||||
export const ACCOUNT_OBJECT_KEY_MIGRATION_STATE = "migrationState";
|
||||
|
||||
/**
|
||||
* A record of which steps have been completed in the libolm to Rust Crypto migration.
|
||||
*
|
||||
* Used by {@link CryptoStore#getMigrationState} and {@link CryptoStore#setMigrationState}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export enum MigrationState {
|
||||
/** No migration steps have yet been completed. */
|
||||
NOT_STARTED,
|
||||
|
||||
/** We have migrated the account data, cross-signing keys, etc. */
|
||||
INITIAL_DATA_MIGRATED,
|
||||
|
||||
/** INITIAL_DATA_MIGRATED, and in addition, we have migrated all the Olm sessions. */
|
||||
OLM_SESSIONS_MIGRATED,
|
||||
|
||||
/** OLM_SESSIONS_MIGRATED, and in addition, we have migrated all the Megolm sessions. */
|
||||
MEGOLM_SESSIONS_MIGRATED,
|
||||
|
||||
/** MEGOLM_SESSIONS_MIGRATED, and in addition, we have migrated all the room settings. */
|
||||
ROOM_SETTINGS_MIGRATED,
|
||||
|
||||
/** ROOM_SETTINGS_MIGRATED, and in addition, we have done the first own keys query in order to
|
||||
* load the public part of the keys that have been migrated */
|
||||
INITIAL_OWN_KEY_QUERY_DONE,
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of batches to be returned by {@link CryptoStore#getEndToEndSessionsBatch} and
|
||||
* {@link CryptoStore#getEndToEndInboundGroupSessionsBatch}.
|
||||
*/
|
||||
export const SESSION_BATCH_SIZE = 50;
|
||||
|
||||
export interface InboundGroupSessionData {
|
||||
room_id: string;
|
||||
/** pickled Olm.InboundGroupSession */
|
||||
session: string;
|
||||
keysClaimed?: Record<string, string>;
|
||||
/** Devices involved in forwarding this session to us (normally empty). */
|
||||
export interface ParkedSharedHistory {
|
||||
senderId: string;
|
||||
senderKey: string;
|
||||
sessionId: string;
|
||||
sessionKey: string;
|
||||
keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; // XXX: Less type dependence on MatrixEvent
|
||||
forwardingCurve25519KeyChain: string[];
|
||||
/** whether this session is untrusted. */
|
||||
untrusted?: boolean;
|
||||
/** whether this session exists during the room being set to shared history. */
|
||||
sharedHistory?: boolean;
|
||||
}
|
||||
|
||||
export interface ICrossSigningInfo {
|
||||
keys: Record<string, CrossSigningKeyInfo>;
|
||||
firstUse: boolean;
|
||||
crossSigningVerifiedBefore: boolean;
|
||||
}
|
||||
|
||||
export interface IRoomEncryption {
|
||||
algorithm: string;
|
||||
rotation_period_ms?: number;
|
||||
rotation_period_msgs?: number;
|
||||
}
|
||||
export enum TrackingStatus {
|
||||
NotTracked,
|
||||
PendingDownload,
|
||||
DownloadInProgress,
|
||||
UpToDate,
|
||||
}
|
||||
|
||||
/**
|
||||
* possible states for a room key request
|
||||
*
|
||||
* The state machine looks like:
|
||||
* ```
|
||||
*
|
||||
* | (cancellation sent)
|
||||
* | .-------------------------------------------------.
|
||||
* | | |
|
||||
* V V (cancellation requested) |
|
||||
* UNSENT -----------------------------+ |
|
||||
* | | |
|
||||
* | | |
|
||||
* | (send successful) | CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
* V | Λ
|
||||
* SENT | |
|
||||
* |-------------------------------- | --------------'
|
||||
* | | (cancellation requested with intent
|
||||
* | | to resend the original request)
|
||||
* | |
|
||||
* | (cancellation requested) |
|
||||
* V |
|
||||
* CANCELLATION_PENDING |
|
||||
* | |
|
||||
* | (cancellation sent) |
|
||||
* V |
|
||||
* (deleted) <---------------------------+
|
||||
* ```
|
||||
*/
|
||||
export enum RoomKeyRequestState {
|
||||
/** request not yet sent */
|
||||
Unsent,
|
||||
/** request sent, awaiting reply */
|
||||
Sent,
|
||||
/** reply received, cancellation not yet sent */
|
||||
CancellationPending,
|
||||
/**
|
||||
* Cancellation not yet sent and will transition to UNSENT instead of
|
||||
* being deleted once the cancellation has been sent.
|
||||
*/
|
||||
CancellationPendingAndWillResend,
|
||||
}
|
||||
|
||||
interface IRoomKey {
|
||||
room_id: string;
|
||||
algorithm: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The parameters of a room key request. The details of the request may
|
||||
* vary with the crypto algorithm, but the management and storage layers for
|
||||
* outgoing requests expect it to have 'room_id' and 'session_id' properties.
|
||||
*/
|
||||
export interface IRoomKeyRequestBody extends IRoomKey {
|
||||
session_id: string;
|
||||
sender_key: string;
|
||||
}
|
||||
|
||||
export interface IRoomKeyRequestRecipient {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
interface IDevice {
|
||||
keys: Record<string, string>;
|
||||
algorithms: string[];
|
||||
verified: DeviceVerification;
|
||||
known: boolean;
|
||||
unsigned?: Record<string, any>;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
/** State of the verification of the device. */
|
||||
export enum DeviceVerification {
|
||||
Blocked = -1,
|
||||
Unverified = 0,
|
||||
Verified = 1,
|
||||
}
|
||||
|
||||
774
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts
generated
vendored
774
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts
generated
vendored
@@ -14,24 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Logger, logger } from "../../logger.ts";
|
||||
import { logger, PrefixedLogger } from "../../logger";
|
||||
import { deepCompare } from "../../utils";
|
||||
import {
|
||||
type CryptoStore,
|
||||
type IDeviceData,
|
||||
type ISession,
|
||||
type SessionExtended,
|
||||
type ISessionInfo,
|
||||
type IWithheld,
|
||||
MigrationState,
|
||||
type Mode,
|
||||
type SecretStorePrivateKeys,
|
||||
SESSION_BATCH_SIZE,
|
||||
ACCOUNT_OBJECT_KEY_MIGRATION_STATE,
|
||||
type InboundGroupSessionData,
|
||||
type IRoomEncryption,
|
||||
} from "./base.ts";
|
||||
import { IndexedDBCryptoStore } from "./indexeddb-crypto-store.ts";
|
||||
import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
CryptoStore,
|
||||
IDeviceData,
|
||||
IProblem,
|
||||
ISession,
|
||||
ISessionInfo,
|
||||
IWithheld,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
SecretStorePrivateKeys,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
|
||||
const PROFILE_TRANSACTIONS = false;
|
||||
|
||||
@@ -39,8 +40,6 @@ const PROFILE_TRANSACTIONS = false;
|
||||
* Implementation of a CryptoStore which is backed by an existing
|
||||
* IndexedDB connection. Generally you want IndexedDBCryptoStore
|
||||
* which connects to the database and defers to one of these.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class Backend implements CryptoStore {
|
||||
private nextTxnId = 0;
|
||||
@@ -57,49 +56,306 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
public async containsData(): Promise<boolean> {
|
||||
throw Error("Not implemented for Backend");
|
||||
}
|
||||
|
||||
public async startup(): Promise<CryptoStore> {
|
||||
// No work to do, as the startup is done by the caller (e.g IndexedDBCryptoStore)
|
||||
// by passing us a ready IDBDatabase instance
|
||||
return this;
|
||||
}
|
||||
|
||||
public async deleteAllData(): Promise<void> {
|
||||
throw Error("This is not implemented, call IDBFactory::deleteDatabase(dbName) instead.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data on how much of the libolm to Rust Crypto migration has been done.
|
||||
* Look for an existing outgoing room key request, and if none is found,
|
||||
* add a new one
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getMigrationState}.
|
||||
*
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}: either the
|
||||
* same instance as passed in, or the existing one.
|
||||
*/
|
||||
public async getMigrationState(): Promise<MigrationState> {
|
||||
let migrationState = MigrationState.NOT_STARTED;
|
||||
await this.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_ACCOUNT);
|
||||
const getReq = objectStore.get(ACCOUNT_OBJECT_KEY_MIGRATION_STATE);
|
||||
getReq.onsuccess = (): void => {
|
||||
migrationState = getReq.result ?? MigrationState.NOT_STARTED;
|
||||
};
|
||||
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
|
||||
const requestBody = request.requestBody;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
|
||||
txn.onerror = reject;
|
||||
|
||||
// first see if we already have an entry for this request.
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
if (existing) {
|
||||
// this entry matches the request - return it.
|
||||
logger.log(
|
||||
`already have key request outstanding for ` +
|
||||
`${requestBody.room_id} / ${requestBody.session_id}: ` +
|
||||
`not sending another`,
|
||||
);
|
||||
resolve(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// we got to the end of the list without finding a match
|
||||
// - add the new request.
|
||||
logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id);
|
||||
txn.oncomplete = (): void => {
|
||||
resolve(request);
|
||||
};
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
store.add(request);
|
||||
});
|
||||
});
|
||||
return migrationState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data on how much of the libolm to Rust Crypto migration has been done.
|
||||
* Look for an existing room key request
|
||||
*
|
||||
* Implementation of {@link CryptoStore.setMigrationState}.
|
||||
* @param requestBody - existing request to look for
|
||||
*
|
||||
* @returns resolves to the matching
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* not found
|
||||
*/
|
||||
public async setMigrationState(migrationState: MigrationState): Promise<void> {
|
||||
await this.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_ACCOUNT);
|
||||
objectStore.put(migrationState, ACCOUNT_OBJECT_KEY_MIGRATION_STATE);
|
||||
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
txn.onerror = reject;
|
||||
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
resolve(existing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* look for an existing room key request in the db
|
||||
*
|
||||
* @internal
|
||||
* @param txn - database transaction
|
||||
* @param requestBody - existing request to look for
|
||||
* @param callback - function to call with the results of the
|
||||
* search. Either passed a matching
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* not found.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private _getOutgoingRoomKeyRequest(
|
||||
txn: IDBTransaction,
|
||||
requestBody: IRoomKeyRequestBody,
|
||||
callback: (req: OutgoingRoomKeyRequest | null) => void,
|
||||
): void {
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
|
||||
const idx = store.index("session");
|
||||
const cursorReq = idx.openCursor([requestBody.room_id, requestBody.session_id]);
|
||||
|
||||
cursorReq.onsuccess = (): void => {
|
||||
const cursor = cursorReq.result;
|
||||
if (!cursor) {
|
||||
// no match found
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = cursor.value;
|
||||
|
||||
if (deepCompare(existing.requestBody, requestBody)) {
|
||||
// got a match
|
||||
callback(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// look at the next entry in the index
|
||||
cursor.continue();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by state
|
||||
*
|
||||
* @param wantedStates - list of acceptable states
|
||||
*
|
||||
* @returns resolves to the a
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* there are no pending requests in those states. If there are multiple
|
||||
* requests in those states, an arbitrary one is chosen.
|
||||
*/
|
||||
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
|
||||
if (wantedStates.length === 0) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// this is a bit tortuous because we need to make sure we do the lookup
|
||||
// in a single transaction, to avoid having a race with the insertion
|
||||
// code.
|
||||
|
||||
// index into the wantedStates array
|
||||
let stateIndex = 0;
|
||||
let result: OutgoingRoomKeyRequest;
|
||||
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
// got a match
|
||||
result = cursor.value;
|
||||
return;
|
||||
}
|
||||
|
||||
// try the next state in the list
|
||||
stateIndex++;
|
||||
if (stateIndex >= wantedStates.length) {
|
||||
// no matches
|
||||
return;
|
||||
}
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
}
|
||||
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = store.index("state").openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
|
||||
return promiseifyTxn(txn).then(() => result);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns All elements in a given state
|
||||
*/
|
||||
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
const index = store.index("state");
|
||||
const request = index.getAll(wantedState);
|
||||
|
||||
request.onsuccess = (): void => resolve(request.result);
|
||||
request.onerror = (): void => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
public getOutgoingRoomKeyRequestsByTarget(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]> {
|
||||
let stateIndex = 0;
|
||||
const results: OutgoingRoomKeyRequest[] = [];
|
||||
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
const keyReq = cursor.value;
|
||||
if (
|
||||
keyReq.recipients.some(
|
||||
(recipient: IRoomKeyRequestRecipient) =>
|
||||
recipient.userId === userId && recipient.deviceId === deviceId,
|
||||
)
|
||||
) {
|
||||
results.push(keyReq);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
// try the next state in the list
|
||||
stateIndex++;
|
||||
if (stateIndex >= wantedStates.length) {
|
||||
// no matches
|
||||
return;
|
||||
}
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
}
|
||||
}
|
||||
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = store.index("state").openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
|
||||
return promiseifyTxn(txn).then(() => results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and update it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
* @param updates - name/value map of updates to apply
|
||||
*
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}
|
||||
* updated request, or null if no matching row was found
|
||||
*/
|
||||
public updateOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
updates: Partial<OutgoingRoomKeyRequest>,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
let result: OutgoingRoomKeyRequest | null = null;
|
||||
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
|
||||
const cursor = this.result;
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
const data = cursor.value;
|
||||
if (data.state != expectedState) {
|
||||
logger.warn(
|
||||
`Cannot update room key request from ${expectedState} ` +
|
||||
`as it was already updated to ${data.state}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
Object.assign(data, updates);
|
||||
cursor.update(data);
|
||||
result = data;
|
||||
}
|
||||
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
|
||||
const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
return promiseifyTxn(txn).then(() => result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and delete it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
*
|
||||
* @returns resolves once the operation is completed
|
||||
*/
|
||||
public deleteOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
|
||||
const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
|
||||
cursorReq.onsuccess = (): void => {
|
||||
const cursor = cursorReq.result;
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
const data = cursor.value;
|
||||
if (data.state != expectedState) {
|
||||
logger.warn(`Cannot delete room key request in state ${data.state} ` + `(expected ${expectedState})`);
|
||||
return;
|
||||
}
|
||||
cursor.delete();
|
||||
};
|
||||
return promiseifyTxn<OutgoingRoomKeyRequest | null>(txn);
|
||||
}
|
||||
|
||||
// Olm Account
|
||||
|
||||
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
|
||||
@@ -121,7 +377,7 @@ export class Backend implements CryptoStore {
|
||||
|
||||
public getCrossSigningKeys(
|
||||
txn: IDBTransaction,
|
||||
func: (keys: Record<string, CrossSigningKeyInfo> | null) => void,
|
||||
func: (keys: Record<string, ICrossSigningKey> | null) => void,
|
||||
): void {
|
||||
const objectStore = txn.objectStore("account");
|
||||
const getReq = objectStore.get("crossSigningKeys");
|
||||
@@ -150,6 +406,11 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
|
||||
const objectStore = txn.objectStore("account");
|
||||
objectStore.put(keys, "crossSigningKeys");
|
||||
}
|
||||
|
||||
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: IDBTransaction,
|
||||
type: K,
|
||||
@@ -224,6 +485,24 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function (): void {
|
||||
try {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
func(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
func(null);
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, <Error>e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public storeEndToEndSession(
|
||||
deviceKey: string,
|
||||
sessionId: string,
|
||||
@@ -239,60 +518,74 @@ export class Backend implements CryptoStore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndSessionsBatch}.
|
||||
*/
|
||||
public async getEndToEndSessionsBatch(): Promise<null | ISessionInfo[]> {
|
||||
const result: ISessionInfo[] = [];
|
||||
await this.doTxn("readonly", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
|
||||
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_SESSIONS);
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function (): void {
|
||||
try {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
result.push(cursor.value);
|
||||
if (result.length < SESSION_BATCH_SIZE) {
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, <Error>e);
|
||||
}
|
||||
};
|
||||
public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
|
||||
const txn = this.db.transaction("session_problems", "readwrite");
|
||||
const objectStore = txn.objectStore("session_problems");
|
||||
objectStore.put({
|
||||
deviceKey,
|
||||
type,
|
||||
fixed,
|
||||
time: Date.now(),
|
||||
});
|
||||
await promiseifyTxn(txn);
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
// No sessions left.
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
|
||||
let result: IProblem | null = null;
|
||||
const txn = this.db.transaction("session_problems", "readwrite");
|
||||
const objectStore = txn.objectStore("session_problems");
|
||||
const index = objectStore.index("deviceKey");
|
||||
const req = index.getAll(deviceKey);
|
||||
req.onsuccess = (): void => {
|
||||
const problems = req.result;
|
||||
if (!problems.length) {
|
||||
result = null;
|
||||
return;
|
||||
}
|
||||
problems.sort((a, b) => {
|
||||
return a.time - b.time;
|
||||
});
|
||||
const lastProblem = problems[problems.length - 1];
|
||||
for (const problem of problems) {
|
||||
if (problem.time > timestamp) {
|
||||
result = Object.assign({}, problem, { fixed: lastProblem.fixed });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (lastProblem.fixed) {
|
||||
result = null;
|
||||
} else {
|
||||
result = lastProblem;
|
||||
}
|
||||
};
|
||||
await promiseifyTxn(txn);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise<void> {
|
||||
await this.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], async (txn) => {
|
||||
try {
|
||||
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_SESSIONS);
|
||||
for (const { deviceKey, sessionId } of sessions) {
|
||||
const req = objectStore.delete([deviceKey, sessionId]);
|
||||
await new Promise((resolve) => {
|
||||
req.onsuccess = resolve;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, <Error>e);
|
||||
}
|
||||
});
|
||||
// FIXME: we should probably prune this when devices get deleted
|
||||
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
||||
const txn = this.db.transaction("notified_error_devices", "readwrite");
|
||||
const objectStore = txn.objectStore("notified_error_devices");
|
||||
|
||||
const ret: IOlmDevice[] = [];
|
||||
|
||||
await Promise.all(
|
||||
devices.map((device) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const { userId, deviceInfo } = device;
|
||||
const getReq = objectStore.get([userId, deviceInfo.deviceId]);
|
||||
getReq.onsuccess = function (): void {
|
||||
if (!getReq.result) {
|
||||
objectStore.put({ userId, deviceId: deviceInfo.deviceId });
|
||||
ret.push(device);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Inbound group sessions
|
||||
@@ -340,6 +633,57 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
|
||||
const objectStore = txn.objectStore("inbound_group_sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function (): void {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
try {
|
||||
func({
|
||||
senderKey: cursor.value.senderCurve25519Key,
|
||||
sessionId: cursor.value.sessionId,
|
||||
sessionData: cursor.value.session,
|
||||
});
|
||||
} catch (e) {
|
||||
abortWithException(txn, <Error>e);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
try {
|
||||
func(null);
|
||||
} catch (e) {
|
||||
abortWithException(txn, <Error>e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: IDBTransaction,
|
||||
): void {
|
||||
const objectStore = txn.objectStore("inbound_group_sessions");
|
||||
const addReq = objectStore.add({
|
||||
senderCurve25519Key,
|
||||
sessionId,
|
||||
session: sessionData,
|
||||
});
|
||||
addReq.onerror = (ev): void => {
|
||||
if (addReq.error?.name === "ConstraintError") {
|
||||
// This stops the error from triggering the txn's onerror
|
||||
ev.stopPropagation();
|
||||
// ...and this stops it from aborting the transaction
|
||||
ev.preventDefault();
|
||||
logger.log("Ignoring duplicate inbound group session: " + senderCurve25519Key + " / " + sessionId);
|
||||
} else {
|
||||
abortWithException(txn, new Error("Failed to add inbound group session: " + addReq.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
@@ -354,94 +698,17 @@ export class Backend implements CryptoStore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of Megolm sessions in the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.countEndToEndInboundGroupSessions}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async countEndToEndInboundGroupSessions(): Promise<number> {
|
||||
let result = 0;
|
||||
await this.doTxn("readonly", [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
const sessionStore = txn.objectStore(IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS);
|
||||
const countReq = sessionStore.count();
|
||||
countReq.onsuccess = (): void => {
|
||||
result = countReq.result;
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndInboundGroupSessionsBatch}.
|
||||
*/
|
||||
public async getEndToEndInboundGroupSessionsBatch(): Promise<null | SessionExtended[]> {
|
||||
const result: SessionExtended[] = [];
|
||||
await this.doTxn(
|
||||
"readonly",
|
||||
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP],
|
||||
(txn) => {
|
||||
const sessionStore = txn.objectStore(IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS);
|
||||
const backupStore = txn.objectStore(IndexedDBCryptoStore.STORE_BACKUP);
|
||||
|
||||
const getReq = sessionStore.openCursor();
|
||||
getReq.onsuccess = function (): void {
|
||||
try {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
const backupGetReq = backupStore.get(cursor.key);
|
||||
backupGetReq.onsuccess = (): void => {
|
||||
result.push({
|
||||
senderKey: cursor.value.senderCurve25519Key,
|
||||
sessionId: cursor.value.sessionId,
|
||||
sessionData: cursor.value.session,
|
||||
needsBackup: backupGetReq.result !== undefined,
|
||||
});
|
||||
if (result.length < SESSION_BATCH_SIZE) {
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, <Error>e);
|
||||
}
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
if (result.length === 0) {
|
||||
// No sessions left.
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async deleteEndToEndInboundGroupSessionsBatch(
|
||||
sessions: { senderKey: string; sessionId: string }[],
|
||||
): Promise<void> {
|
||||
await this.doTxn("readwrite", [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], async (txn) => {
|
||||
try {
|
||||
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS);
|
||||
for (const { senderKey, sessionId } of sessions) {
|
||||
const req = objectStore.delete([senderKey, sessionId]);
|
||||
await new Promise((resolve) => {
|
||||
req.onsuccess = resolve;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, <Error>e);
|
||||
}
|
||||
public storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: IWithheld,
|
||||
txn: IDBTransaction,
|
||||
): void {
|
||||
const objectStore = txn.objectStore("inbound_group_sessions_withheld");
|
||||
objectStore.put({
|
||||
senderCurve25519Key,
|
||||
sessionId,
|
||||
session: sessionData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -457,6 +724,16 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
|
||||
const objectStore = txn.objectStore("device_data");
|
||||
objectStore.put(deviceData, "-");
|
||||
}
|
||||
|
||||
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
|
||||
const objectStore = txn.objectStore("rooms");
|
||||
objectStore.put(roomInfo, roomId);
|
||||
}
|
||||
|
||||
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
|
||||
const rooms: Parameters<Parameters<Backend["getEndToEndRooms"]>[1]>[0] = {};
|
||||
const objectStore = txn.objectStore("rooms");
|
||||
@@ -476,6 +753,67 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
// session backups
|
||||
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sessions: ISession[] = [];
|
||||
|
||||
const txn = this.db.transaction(["sessions_needing_backup", "inbound_group_sessions"], "readonly");
|
||||
txn.onerror = reject;
|
||||
txn.oncomplete = function (): void {
|
||||
resolve(sessions);
|
||||
};
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
const sessionStore = txn.objectStore("inbound_group_sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function (): void {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
const sessionGetReq = sessionStore.get(cursor.key);
|
||||
sessionGetReq.onsuccess = function (): void {
|
||||
sessions.push({
|
||||
senderKey: sessionGetReq.result.senderCurve25519Key,
|
||||
sessionId: sessionGetReq.result.sessionId,
|
||||
sessionData: sessionGetReq.result.session,
|
||||
});
|
||||
};
|
||||
if (!limit || sessions.length < limit) {
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("sessions_needing_backup", "readonly");
|
||||
}
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = objectStore.count();
|
||||
req.onerror = reject;
|
||||
req.onsuccess = (): void => resolve(req.result);
|
||||
});
|
||||
}
|
||||
|
||||
public async unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("sessions_needing_backup", "readwrite");
|
||||
}
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
await Promise.all(
|
||||
sessions.map((session) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = objectStore.delete([session.senderKey, session.sessionId]);
|
||||
req.onsuccess = resolve;
|
||||
req.onerror = reject;
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("sessions_needing_backup", "readwrite");
|
||||
@@ -495,11 +833,80 @@ export class Backend implements CryptoStore {
|
||||
);
|
||||
}
|
||||
|
||||
public addSharedHistoryInboundGroupSession(
|
||||
roomId: string,
|
||||
senderKey: string,
|
||||
sessionId: string,
|
||||
txn?: IDBTransaction,
|
||||
): void {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("shared_history_inbound_group_sessions", "readwrite");
|
||||
}
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
req.onsuccess = (): void => {
|
||||
const { sessions } = req.result || { sessions: [] };
|
||||
sessions.push([senderKey, sessionId]);
|
||||
objectStore.put({ roomId, sessions });
|
||||
};
|
||||
}
|
||||
|
||||
public getSharedHistoryInboundGroupSessions(
|
||||
roomId: string,
|
||||
txn?: IDBTransaction,
|
||||
): Promise<[senderKey: string, sessionId: string][]> {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("shared_history_inbound_group_sessions", "readonly");
|
||||
}
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = (): void => {
|
||||
const { sessions } = req.result || { sessions: [] };
|
||||
resolve(sessions);
|
||||
};
|
||||
req.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory, txn?: IDBTransaction): void {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("parked_shared_history", "readwrite");
|
||||
}
|
||||
const objectStore = txn.objectStore("parked_shared_history");
|
||||
const req = objectStore.get([roomId]);
|
||||
req.onsuccess = (): void => {
|
||||
const { parked } = req.result || { parked: [] };
|
||||
parked.push(parkedData);
|
||||
objectStore.put({ roomId, parked });
|
||||
};
|
||||
}
|
||||
|
||||
public takeParkedSharedHistory(roomId: string, txn?: IDBTransaction): Promise<ParkedSharedHistory[]> {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("parked_shared_history", "readwrite");
|
||||
}
|
||||
const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId);
|
||||
return new Promise((resolve, reject) => {
|
||||
cursorReq.onsuccess = (): void => {
|
||||
const cursor = cursorReq.result;
|
||||
if (!cursor) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
const data = cursor.value;
|
||||
cursor.delete();
|
||||
resolve(data);
|
||||
};
|
||||
cursorReq.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
public doTxn<T>(
|
||||
mode: Mode,
|
||||
stores: string | string[],
|
||||
func: (txn: IDBTransaction) => T,
|
||||
log: Logger = logger,
|
||||
log: PrefixedLogger = logger,
|
||||
): Promise<T> {
|
||||
let startTime: number;
|
||||
let description: string;
|
||||
@@ -590,7 +997,7 @@ const DB_MIGRATIONS: DbMigration[] = [
|
||||
export const VERSION = DB_MIGRATIONS.length;
|
||||
|
||||
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
|
||||
logger.log(`Upgrading IndexedDBCryptoStore from version ${oldVersion} to ${VERSION}`);
|
||||
logger.log(`Upgrading IndexedDBCryptoStore from version ${oldVersion}` + ` to ${VERSION}`);
|
||||
DB_MIGRATIONS.forEach((migration, index) => {
|
||||
if (oldVersion <= index) migration(db);
|
||||
});
|
||||
@@ -607,7 +1014,7 @@ function createDatabase(db: IDBDatabase): void {
|
||||
}
|
||||
|
||||
interface IWrappedIDBTransaction extends IDBTransaction {
|
||||
_mx_abortexception: Error;
|
||||
_mx_abortexception: Error; // eslint-disable-line camelcase
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -621,7 +1028,7 @@ function abortWithException(txn: IDBTransaction, e: Error): void {
|
||||
(txn as IWrappedIDBTransaction)._mx_abortexception = e;
|
||||
try {
|
||||
txn.abort();
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// sometimes we won't be able to abort the transaction
|
||||
// (ie. if it's aborted or completed)
|
||||
}
|
||||
@@ -632,7 +1039,6 @@ function promiseifyTxn<T>(txn: IDBTransaction): Promise<T | null> {
|
||||
txn.oncomplete = (): void => {
|
||||
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
|
||||
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
|
||||
return;
|
||||
}
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
435
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts
generated
vendored
435
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts
generated
vendored
@@ -14,28 +14,31 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger, type Logger } from "../../logger.ts";
|
||||
import { LocalStorageCryptoStore } from "./localStorage-crypto-store.ts";
|
||||
import { MemoryCryptoStore } from "./memory-crypto-store.ts";
|
||||
import * as IndexedDBCryptoStoreBackend from "./indexeddb-crypto-store-backend.ts";
|
||||
import { InvalidCryptoStoreError, InvalidCryptoStoreState } from "../../errors.ts";
|
||||
import * as IndexedDBHelpers from "../../indexeddb-helpers.ts";
|
||||
import { logger, PrefixedLogger } from "../../logger";
|
||||
import { LocalStorageCryptoStore } from "./localStorage-crypto-store";
|
||||
import { MemoryCryptoStore } from "./memory-crypto-store";
|
||||
import * as IndexedDBCryptoStoreBackend from "./indexeddb-crypto-store-backend";
|
||||
import { InvalidCryptoStoreError, InvalidCryptoStoreState } from "../../errors";
|
||||
import * as IndexedDBHelpers from "../../indexeddb-helpers";
|
||||
import {
|
||||
type CryptoStore,
|
||||
type ISession,
|
||||
type SessionExtended,
|
||||
type ISessionInfo,
|
||||
type IWithheld,
|
||||
MigrationState,
|
||||
type Mode,
|
||||
type SecretStorePrivateKeys,
|
||||
ACCOUNT_OBJECT_KEY_MIGRATION_STATE,
|
||||
type InboundGroupSessionData,
|
||||
type IRoomEncryption,
|
||||
} from "./base.ts";
|
||||
import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
CryptoStore,
|
||||
IDeviceData,
|
||||
IProblem,
|
||||
ISession,
|
||||
ISessionInfo,
|
||||
IWithheld,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
SecretStorePrivateKeys,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
|
||||
/*
|
||||
/**
|
||||
* Internal module. indexeddb storage for e2e.
|
||||
*/
|
||||
|
||||
@@ -58,52 +61,6 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
return IndexedDBHelpers.exists(indexedDB, dbName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to check if a legacy crypto store exists and has not been migrated.
|
||||
* Returns true if the store exists and has not been migrated, false otherwise.
|
||||
*/
|
||||
public static existsAndIsNotMigrated(indexedDb: IDBFactory, dbName: string): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
let exists = true;
|
||||
const openDBRequest = indexedDb.open(dbName);
|
||||
openDBRequest.onupgradeneeded = (): void => {
|
||||
// Since we did not provide an explicit version when opening, this event
|
||||
// should only fire if the DB did not exist before at any version.
|
||||
exists = false;
|
||||
};
|
||||
openDBRequest.onblocked = (): void => reject(openDBRequest.error);
|
||||
openDBRequest.onsuccess = (): void => {
|
||||
const db = openDBRequest.result;
|
||||
if (!exists) {
|
||||
db.close();
|
||||
// The DB did not exist before, but has been created as part of this
|
||||
// existence check. Delete it now to restore previous state. Delete can
|
||||
// actually take a while to complete in some browsers, so don't wait for
|
||||
// it. This won't block future open calls that a store might issue next to
|
||||
// properly set up the DB.
|
||||
indexedDb.deleteDatabase(dbName);
|
||||
resolve(false);
|
||||
} else {
|
||||
const tx = db.transaction([IndexedDBCryptoStore.STORE_ACCOUNT], "readonly");
|
||||
const objectStore = tx.objectStore(IndexedDBCryptoStore.STORE_ACCOUNT);
|
||||
const getReq = objectStore.get(ACCOUNT_OBJECT_KEY_MIGRATION_STATE);
|
||||
|
||||
getReq.onsuccess = (): void => {
|
||||
const migrationState = getReq.result ?? MigrationState.NOT_STARTED;
|
||||
resolve(migrationState === MigrationState.NOT_STARTED);
|
||||
};
|
||||
|
||||
getReq.onerror = (): void => {
|
||||
reject(getReq.error);
|
||||
};
|
||||
|
||||
db.close();
|
||||
}
|
||||
};
|
||||
openDBRequest.onerror = (): void => reject(openDBRequest.error);
|
||||
});
|
||||
}
|
||||
|
||||
private backendPromise?: Promise<CryptoStore>;
|
||||
private backend?: CryptoStore;
|
||||
|
||||
@@ -113,21 +70,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
* @param indexedDB - global indexedDB instance
|
||||
* @param dbName - name of db to connect to
|
||||
*/
|
||||
public constructor(
|
||||
private readonly indexedDB: IDBFactory,
|
||||
private readonly dbName: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns true if this CryptoStore has ever been initialised (ie, it might contain data).
|
||||
*
|
||||
* Implementation of {@link CryptoStore.containsData}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async containsData(): Promise<boolean> {
|
||||
return IndexedDBCryptoStore.exists(this.indexedDB, this.dbName);
|
||||
}
|
||||
public constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
|
||||
|
||||
/**
|
||||
* Ensure the database exists and is up-to-date, or fall back to
|
||||
@@ -199,15 +142,14 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
// in this db so we should use it or nothing at all.
|
||||
throw new InvalidCryptoStoreError(InvalidCryptoStoreState.TooNew);
|
||||
}
|
||||
logger.warn(`unable to connect to indexeddb ${this.dbName}: falling back to localStorage store: ${e}`);
|
||||
logger.warn(
|
||||
`unable to connect to indexeddb ${this.dbName}` + `: falling back to localStorage store: ${e}`,
|
||||
);
|
||||
|
||||
try {
|
||||
if (!(globalThis.localStorage instanceof Storage)) {
|
||||
throw new Error("localStorage is not available");
|
||||
}
|
||||
return new LocalStorageCryptoStore(globalThis.localStorage);
|
||||
return new LocalStorageCryptoStore(global.localStorage);
|
||||
} catch (e) {
|
||||
logger.warn(`Unable to open localStorage: falling back to in-memory store: ${e}`);
|
||||
logger.warn(`unable to open localStorage: falling back to in-memory store: ${e}`);
|
||||
return new MemoryCryptoStore();
|
||||
}
|
||||
})
|
||||
@@ -256,25 +198,107 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data on how much of the libolm to Rust Crypto migration has been done.
|
||||
* Look for an existing outgoing room key request, and if none is found,
|
||||
* add a new one
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getMigrationState}.
|
||||
*
|
||||
* @internal
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}: either the
|
||||
* same instance as passed in, or the existing one.
|
||||
*/
|
||||
public getMigrationState(): Promise<MigrationState> {
|
||||
return this.backend!.getMigrationState();
|
||||
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
|
||||
return this.backend!.getOrAddOutgoingRoomKeyRequest(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data on how much of the libolm to Rust Crypto migration has been done.
|
||||
* Look for an existing room key request
|
||||
*
|
||||
* Implementation of {@link CryptoStore.setMigrationState}.
|
||||
* @param requestBody - existing request to look for
|
||||
*
|
||||
* @internal
|
||||
* @returns resolves to the matching
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* not found
|
||||
*/
|
||||
public setMigrationState(migrationState: MigrationState): Promise<void> {
|
||||
return this.backend!.setMigrationState(migrationState);
|
||||
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return this.backend!.getOutgoingRoomKeyRequest(requestBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by state
|
||||
*
|
||||
* @param wantedStates - list of acceptable states
|
||||
*
|
||||
* @returns resolves to the a
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* there are no pending requests in those states. If there are multiple
|
||||
* requests in those states, an arbitrary one is chosen.
|
||||
*/
|
||||
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return this.backend!.getOutgoingRoomKeyRequestByState(wantedStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by state –
|
||||
* unlike above, return a list of all entries in one state.
|
||||
*
|
||||
* @returns Returns an array of requests in the given state
|
||||
*/
|
||||
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
|
||||
return this.backend!.getAllOutgoingRoomKeyRequestsByState(wantedState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by target device and state
|
||||
*
|
||||
* @param userId - Target user ID
|
||||
* @param deviceId - Target device ID
|
||||
* @param wantedStates - list of acceptable states
|
||||
*
|
||||
* @returns resolves to a list of all the
|
||||
* {@link OutgoingRoomKeyRequest}
|
||||
*/
|
||||
public getOutgoingRoomKeyRequestsByTarget(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]> {
|
||||
return this.backend!.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and update it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
* @param updates - name/value map of updates to apply
|
||||
*
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}
|
||||
* updated request, or null if no matching row was found
|
||||
*/
|
||||
public updateOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
updates: Partial<OutgoingRoomKeyRequest>,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return this.backend!.updateOutgoingRoomKeyRequest(requestId, expectedState, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and delete it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
*
|
||||
* @returns resolves once the operation is completed
|
||||
*/
|
||||
public deleteOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return this.backend!.deleteOutgoingRoomKeyRequest(requestId, expectedState);
|
||||
}
|
||||
|
||||
// Olm Account
|
||||
@@ -311,7 +335,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
*/
|
||||
public getCrossSigningKeys(
|
||||
txn: IDBTransaction,
|
||||
func: (keys: Record<string, CrossSigningKeyInfo> | null) => void,
|
||||
func: (keys: Record<string, ICrossSigningKey> | null) => void,
|
||||
): void {
|
||||
this.backend!.getCrossSigningKeys(txn, func);
|
||||
}
|
||||
@@ -329,6 +353,16 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.getSecretStorePrivateKey(txn, func, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the cross-signing keys back to the store
|
||||
*
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
* @param keys - keys object as getCrossSigningKeys()
|
||||
*/
|
||||
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
|
||||
this.backend!.storeCrossSigningKeys(txn, keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the cross-signing private keys back to the store
|
||||
*
|
||||
@@ -395,6 +429,17 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.getEndToEndSessions(deviceKey, txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all end-to-end sessions
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
* @param func - Called one for each session with
|
||||
* an object with, deviceKey, lastReceivedMessageTs, sessionId
|
||||
* and session keys.
|
||||
*/
|
||||
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
|
||||
this.backend!.getAllEndToEndSessions(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a session between the logged-in user and another device
|
||||
* @param deviceKey - The public key of the other device.
|
||||
@@ -411,37 +456,16 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of Megolm sessions in the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.countEndToEndInboundGroupSessions}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public countEndToEndInboundGroupSessions(): Promise<number> {
|
||||
return this.backend!.countEndToEndInboundGroupSessions();
|
||||
public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
|
||||
return this.backend!.storeEndToEndSessionProblem(deviceKey, type, fixed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public getEndToEndSessionsBatch(): Promise<null | ISessionInfo[]> {
|
||||
return this.backend!.getEndToEndSessionsBatch();
|
||||
public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
|
||||
return this.backend!.getEndToEndSessionProblem(deviceKey, timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise<void> {
|
||||
return this.backend!.deleteEndToEndSessionsBatch(sessions);
|
||||
public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
||||
return this.backend!.filterOutNotifiedErrorDevices(devices);
|
||||
}
|
||||
|
||||
// Inbound group sessions
|
||||
@@ -464,6 +488,35 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all inbound group sessions in the store
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
* @param func - Called once for each group session
|
||||
* in the store with an object having keys `{senderKey, sessionId, sessionData}`,
|
||||
* then once with null to indicate the end of the list.
|
||||
*/
|
||||
public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
|
||||
this.backend!.getAllEndToEndInboundGroupSessions(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an end-to-end inbound group session to the store.
|
||||
* If there already exists an inbound group session with the same
|
||||
* senderCurve25519Key and sessionID, the session will not be added.
|
||||
* @param senderCurve25519Key - The sender's curve 25519 key
|
||||
* @param sessionId - The ID of the session
|
||||
* @param sessionData - The session data structure
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
*/
|
||||
public addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: IDBTransaction,
|
||||
): void {
|
||||
this.backend!.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an end-to-end inbound group session to the store.
|
||||
* If there already exists an inbound group session with the same
|
||||
@@ -482,28 +535,51 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
|
||||
}
|
||||
|
||||
public storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: IWithheld,
|
||||
txn: IDBTransaction,
|
||||
): void {
|
||||
this.backend!.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn);
|
||||
}
|
||||
|
||||
// End-to-end device tracking
|
||||
|
||||
/**
|
||||
* Fetch a batch of Megolm sessions from the database.
|
||||
* Store the state of all tracked devices
|
||||
* This contains devices for each user, a tracking state for each user
|
||||
* and a sync token matching the point in time the snapshot represents.
|
||||
* These all need to be written out in full each time such that the snapshot
|
||||
* is always consistent, so they are stored in one object.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
*/
|
||||
public getEndToEndInboundGroupSessionsBatch(): Promise<SessionExtended[] | null> {
|
||||
return this.backend!.getEndToEndInboundGroupSessionsBatch();
|
||||
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
|
||||
this.backend!.storeEndToEndDeviceData(deviceData, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Megolm sessions from the database.
|
||||
* Get the state of all tracked devices
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
* @param func - Function called with the
|
||||
* device data
|
||||
*/
|
||||
public deleteEndToEndInboundGroupSessionsBatch(
|
||||
sessions: { senderKey: string; sessionId: string }[],
|
||||
): Promise<void> {
|
||||
return this.backend!.deleteEndToEndInboundGroupSessionsBatch(sessions);
|
||||
public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
|
||||
this.backend!.getEndToEndDeviceData(txn, func);
|
||||
}
|
||||
|
||||
// End to End Rooms
|
||||
|
||||
/**
|
||||
* Store the end-to-end state for a room.
|
||||
* @param roomId - The room's ID.
|
||||
* @param roomInfo - The end-to-end info for the room.
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
*/
|
||||
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
|
||||
this.backend!.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -515,6 +591,37 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.getEndToEndRooms(txn, func);
|
||||
}
|
||||
|
||||
// session backups
|
||||
|
||||
/**
|
||||
* Get the inbound group sessions that need to be backed up.
|
||||
* @param limit - The maximum number of sessions to retrieve. 0
|
||||
* for no limit.
|
||||
* @returns resolves to an array of inbound group sessions
|
||||
*/
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
return this.backend!.getSessionsNeedingBackup(limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the inbound group sessions that need to be backed up.
|
||||
* @param txn - An active transaction. See doTxn(). (optional)
|
||||
* @returns resolves to the number of sessions
|
||||
*/
|
||||
public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
|
||||
return this.backend!.countSessionsNeedingBackup(txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmark sessions as needing to be backed up.
|
||||
* @param sessions - The sessions that need to be backed up.
|
||||
* @param txn - An active transaction. See doTxn(). (optional)
|
||||
* @returns resolves when the sessions are unmarked
|
||||
*/
|
||||
public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
|
||||
return this.backend!.unmarkSessionsNeedingBackup(sessions, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark sessions as needing to be backed up.
|
||||
* @param sessions - The sessions that need to be backed up.
|
||||
@@ -525,6 +632,49 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
return this.backend!.markSessionsNeedingBackup(sessions, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared-history group session for a room.
|
||||
* @param roomId - The room that the key belongs to
|
||||
* @param senderKey - The sender's curve 25519 key
|
||||
* @param sessionId - The ID of the session
|
||||
* @param txn - An active transaction. See doTxn(). (optional)
|
||||
*/
|
||||
public addSharedHistoryInboundGroupSession(
|
||||
roomId: string,
|
||||
senderKey: string,
|
||||
sessionId: string,
|
||||
txn?: IDBTransaction,
|
||||
): void {
|
||||
this.backend!.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared-history group session for a room.
|
||||
* @param roomId - The room that the key belongs to
|
||||
* @param txn - An active transaction. See doTxn(). (optional)
|
||||
* @returns Promise which resolves to an array of [senderKey, sessionId]
|
||||
*/
|
||||
public getSharedHistoryInboundGroupSessions(
|
||||
roomId: string,
|
||||
txn?: IDBTransaction,
|
||||
): Promise<[senderKey: string, sessionId: string][]> {
|
||||
return this.backend!.getSharedHistoryInboundGroupSessions(roomId, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Park a shared-history group session for a room we may be invited to later.
|
||||
*/
|
||||
public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory, txn?: IDBTransaction): void {
|
||||
this.backend!.addParkedSharedHistory(roomId, parkedData, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop out all shared-history group sessions for a room.
|
||||
*/
|
||||
public takeParkedSharedHistory(roomId: string, txn?: IDBTransaction): Promise<ParkedSharedHistory[]> {
|
||||
return this.backend!.takeParkedSharedHistory(roomId, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a transaction on the crypto store. Any store methods
|
||||
* that require a transaction (txn) object to be passed in may
|
||||
@@ -547,7 +697,12 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
* reject with that exception. On synchronous backends, the
|
||||
* exception will propagate to the caller of the getFoo method.
|
||||
*/
|
||||
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: IDBTransaction) => T, log?: Logger): Promise<T> {
|
||||
public doTxn<T>(
|
||||
mode: Mode,
|
||||
stores: Iterable<string>,
|
||||
func: (txn: IDBTransaction) => T,
|
||||
log?: PrefixedLogger,
|
||||
): Promise<T> {
|
||||
return this.backend!.doTxn<T>(mode, stores, func as (txn: unknown) => T, log);
|
||||
}
|
||||
}
|
||||
|
||||
343
node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts
generated
vendored
343
node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts
generated
vendored
@@ -14,22 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "../../logger.ts";
|
||||
import { MemoryCryptoStore } from "./memory-crypto-store.ts";
|
||||
import {
|
||||
type CryptoStore,
|
||||
type ISession,
|
||||
type SessionExtended,
|
||||
type ISessionInfo,
|
||||
type IWithheld,
|
||||
MigrationState,
|
||||
type Mode,
|
||||
type SecretStorePrivateKeys,
|
||||
SESSION_BATCH_SIZE,
|
||||
type InboundGroupSessionData,
|
||||
type IRoomEncryption,
|
||||
} from "./base.ts";
|
||||
import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
import { logger } from "../../logger";
|
||||
import { MemoryCryptoStore } from "./memory-crypto-store";
|
||||
import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode, SecretStorePrivateKeys } from "./base";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { safeSet } from "../../utils";
|
||||
|
||||
/**
|
||||
* Internal module. Partial localStorage backed storage for e2e.
|
||||
@@ -40,9 +32,10 @@ import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
*/
|
||||
|
||||
const E2E_PREFIX = "crypto.";
|
||||
const KEY_END_TO_END_MIGRATION_STATE = E2E_PREFIX + "migration";
|
||||
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
|
||||
const KEY_NOTIFIED_ERROR_DEVICES = E2E_PREFIX + "notified_error_devices";
|
||||
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
|
||||
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
|
||||
const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/";
|
||||
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
|
||||
@@ -52,6 +45,10 @@ function keyEndToEndSessions(deviceKey: string): string {
|
||||
return E2E_PREFIX + "sessions/" + deviceKey;
|
||||
}
|
||||
|
||||
function keyEndToEndSessionProblems(deviceKey: string): string {
|
||||
return E2E_PREFIX + "session.problems/" + deviceKey;
|
||||
}
|
||||
|
||||
function keyEndToEndInboundGroupSession(senderKey: string, sessionId: string): string {
|
||||
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
|
||||
}
|
||||
@@ -64,7 +61,7 @@ function keyEndToEndRoomsPrefix(roomId: string): string {
|
||||
return KEY_ROOMS_PREFIX + roomId;
|
||||
}
|
||||
|
||||
export class LocalStorageCryptoStore extends MemoryCryptoStore implements CryptoStore {
|
||||
export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
public static exists(store: Storage): boolean {
|
||||
const length = store.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
@@ -79,53 +76,17 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this CryptoStore has ever been initialised (ie, it might contain data).
|
||||
*
|
||||
* Implementation of {@link CryptoStore.containsData}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async containsData(): Promise<boolean> {
|
||||
return LocalStorageCryptoStore.exists(this.store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data on how much of the libolm to Rust Crypto migration has been done.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getMigrationState}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getMigrationState(): Promise<MigrationState> {
|
||||
return getJsonItem(this.store, KEY_END_TO_END_MIGRATION_STATE) ?? MigrationState.NOT_STARTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data on how much of the libolm to Rust Crypto migration has been done.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.setMigrationState}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async setMigrationState(migrationState: MigrationState): Promise<void> {
|
||||
setJsonItem(this.store, KEY_END_TO_END_MIGRATION_STATE, migrationState);
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
const key = this.store.key(i);
|
||||
if (key?.startsWith(keyEndToEndSessions(""))) {
|
||||
const sessions = getJsonItem(this.store, key);
|
||||
count += Object.keys(sessions ?? {}).length;
|
||||
}
|
||||
if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) ++count;
|
||||
}
|
||||
func(count);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private _getEndToEndSessions(deviceKey: string): Record<string, ISessionInfo> {
|
||||
const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
|
||||
const fixedSessions: Record<string, ISessionInfo> = {};
|
||||
@@ -151,7 +112,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
func: (session: ISessionInfo) => void,
|
||||
): void {
|
||||
const sessions = this._getEndToEndSessions(deviceKey);
|
||||
func(sessions[sessionId] ?? {});
|
||||
func(sessions[sessionId] || {});
|
||||
}
|
||||
|
||||
public getEndToEndSessions(
|
||||
@@ -159,7 +120,18 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
txn: unknown,
|
||||
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
|
||||
): void {
|
||||
func(this._getEndToEndSessions(deviceKey) ?? {});
|
||||
func(this._getEndToEndSessions(deviceKey) || {});
|
||||
}
|
||||
|
||||
public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) {
|
||||
const deviceKey = this.store.key(i)!.split("/")[1];
|
||||
for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) {
|
||||
func(sess);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
|
||||
@@ -168,54 +140,56 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getEndToEndSessionsBatch(): Promise<null | ISessionInfo[]> {
|
||||
const result: ISessionInfo[] = [];
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) {
|
||||
const deviceKey = this.store.key(i)!.split("/")[1];
|
||||
for (const session of Object.values(this._getEndToEndSessions(deviceKey))) {
|
||||
result.push(session);
|
||||
if (result.length >= SESSION_BATCH_SIZE) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
// No sessions left.
|
||||
return null;
|
||||
}
|
||||
|
||||
// There are fewer sessions than the batch size; return the final batch of sessions.
|
||||
return result;
|
||||
public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
|
||||
const key = keyEndToEndSessionProblems(deviceKey);
|
||||
const problems = getJsonItem<IProblem[]>(this.store, key) || [];
|
||||
problems.push({ type, fixed, time: Date.now() });
|
||||
problems.sort((a, b) => {
|
||||
return a.time - b.time;
|
||||
});
|
||||
setJsonItem(this.store, key, problems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise<void> {
|
||||
for (const { deviceKey, sessionId } of sessions) {
|
||||
const deviceSessions = this._getEndToEndSessions(deviceKey) || {};
|
||||
delete deviceSessions[sessionId];
|
||||
if (Object.keys(deviceSessions).length === 0) {
|
||||
// No more sessions for this device.
|
||||
this.store.removeItem(keyEndToEndSessions(deviceKey));
|
||||
} else {
|
||||
setJsonItem(this.store, keyEndToEndSessions(deviceKey), deviceSessions);
|
||||
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
|
||||
const key = keyEndToEndSessionProblems(deviceKey);
|
||||
const problems = getJsonItem<IProblem[]>(this.store, key) || [];
|
||||
if (!problems.length) {
|
||||
return null;
|
||||
}
|
||||
const lastProblem = problems[problems.length - 1];
|
||||
for (const problem of problems) {
|
||||
if (problem.time > timestamp) {
|
||||
return Object.assign({}, problem, { fixed: lastProblem.fixed });
|
||||
}
|
||||
}
|
||||
if (lastProblem.fixed) {
|
||||
return null;
|
||||
} else {
|
||||
return lastProblem;
|
||||
}
|
||||
}
|
||||
|
||||
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
||||
const notifiedErrorDevices =
|
||||
getJsonItem<MemoryCryptoStore["notifiedErrorDevices"]>(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
|
||||
const ret: IOlmDevice[] = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const { userId, deviceInfo } = device;
|
||||
if (userId in notifiedErrorDevices) {
|
||||
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
|
||||
ret.push(device);
|
||||
safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
|
||||
}
|
||||
} else {
|
||||
ret.push(device);
|
||||
safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
|
||||
}
|
||||
}
|
||||
|
||||
setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Inbound Group Sessions
|
||||
@@ -232,6 +206,37 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
);
|
||||
}
|
||||
|
||||
public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
const key = this.store.key(i);
|
||||
if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
|
||||
// we can't use split, as the components we are trying to split out
|
||||
// might themselves contain '/' characters. We rely on the
|
||||
// senderKey being a (32-byte) curve25519 key, base64-encoded
|
||||
// (hence 43 characters long).
|
||||
|
||||
func({
|
||||
senderKey: key.slice(KEY_INBOUND_SESSION_PREFIX.length, KEY_INBOUND_SESSION_PREFIX.length + 43),
|
||||
sessionId: key.slice(KEY_INBOUND_SESSION_PREFIX.length + 44),
|
||||
sessionData: getJsonItem(this.store, key)!,
|
||||
});
|
||||
}
|
||||
}
|
||||
func(null);
|
||||
}
|
||||
|
||||
public addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: unknown,
|
||||
): void {
|
||||
const existing = getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId));
|
||||
if (!existing) {
|
||||
this.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
|
||||
}
|
||||
}
|
||||
|
||||
public storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
@@ -241,80 +246,25 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
setJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), sessionData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of Megolm sessions in the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.countEndToEndInboundGroupSessions}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async countEndToEndInboundGroupSessions(): Promise<number> {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
const key = this.store.key(i);
|
||||
if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
public storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: IWithheld,
|
||||
txn: unknown,
|
||||
): void {
|
||||
setJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId), sessionData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getEndToEndInboundGroupSessionsBatch(): Promise<SessionExtended[] | null> {
|
||||
const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
const result: SessionExtended[] = [];
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
const key = this.store.key(i);
|
||||
if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
|
||||
const key2 = key.slice(KEY_INBOUND_SESSION_PREFIX.length);
|
||||
|
||||
// we can't use split, as the components we are trying to split out
|
||||
// might themselves contain '/' characters. We rely on the
|
||||
// senderKey being a (32-byte) curve25519 key, base64-encoded
|
||||
// (hence 43 characters long).
|
||||
|
||||
result.push({
|
||||
senderKey: key2.slice(0, 43),
|
||||
sessionId: key2.slice(44),
|
||||
sessionData: getJsonItem(this.store, key)!,
|
||||
needsBackup: key2 in sessionsNeedingBackup,
|
||||
});
|
||||
|
||||
if (result.length >= SESSION_BATCH_SIZE) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
// No sessions left.
|
||||
return null;
|
||||
}
|
||||
|
||||
// There are fewer sessions than the batch size; return the final batch of sessions.
|
||||
return result;
|
||||
public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
|
||||
func(getJsonItem(this.store, KEY_DEVICE_DATA));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async deleteEndToEndInboundGroupSessionsBatch(
|
||||
sessions: { senderKey: string; sessionId: string }[],
|
||||
): Promise<void> {
|
||||
for (const { senderKey, sessionId } of sessions) {
|
||||
const k = keyEndToEndInboundGroupSession(senderKey, sessionId);
|
||||
this.store.removeItem(k);
|
||||
}
|
||||
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
|
||||
setJsonItem(this.store, KEY_DEVICE_DATA, deviceData);
|
||||
}
|
||||
|
||||
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
|
||||
setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo);
|
||||
}
|
||||
|
||||
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
|
||||
@@ -331,6 +281,47 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
func(result);
|
||||
}
|
||||
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
const sessions: ISession[] = [];
|
||||
|
||||
for (const session in sessionsNeedingBackup) {
|
||||
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
|
||||
// see getAllEndToEndInboundGroupSessions for the magic number explanations
|
||||
const senderKey = session.slice(0, 43);
|
||||
const sessionId = session.slice(44);
|
||||
this.getEndToEndInboundGroupSession(senderKey, sessionId, null, (sessionData) => {
|
||||
sessions.push({
|
||||
senderKey: senderKey,
|
||||
sessionId: sessionId,
|
||||
sessionData: sessionData!,
|
||||
});
|
||||
});
|
||||
if (limit && sessions.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(sessions);
|
||||
}
|
||||
|
||||
public countSessionsNeedingBackup(): Promise<number> {
|
||||
const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
|
||||
}
|
||||
|
||||
public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
|
||||
const sessionsNeedingBackup =
|
||||
getJsonItem<{
|
||||
[senderKeySessionId: string]: string;
|
||||
}>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
for (const session of sessions) {
|
||||
delete sessionsNeedingBackup[session.senderKey + "/" + session.sessionId];
|
||||
}
|
||||
setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
|
||||
const sessionsNeedingBackup =
|
||||
getJsonItem<{
|
||||
@@ -364,8 +355,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
|
||||
}
|
||||
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void {
|
||||
const keys = getJsonItem<Record<string, CrossSigningKeyInfo>>(this.store, KEY_CROSS_SIGNING_KEYS);
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
|
||||
const keys = getJsonItem<Record<string, ICrossSigningKey>>(this.store, KEY_CROSS_SIGNING_KEYS);
|
||||
func(keys);
|
||||
}
|
||||
|
||||
@@ -378,6 +369,10 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
func(key);
|
||||
}
|
||||
|
||||
public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
|
||||
setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
|
||||
}
|
||||
|
||||
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: unknown,
|
||||
type: K,
|
||||
|
||||
500
node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts
generated
vendored
500
node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts
generated
vendored
@@ -14,61 +14,47 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { safeSet } from "../../utils.ts";
|
||||
import { logger } from "../../logger";
|
||||
import { safeSet, deepCompare, promiseTry } from "../../utils";
|
||||
import {
|
||||
type CryptoStore,
|
||||
type ISession,
|
||||
type SessionExtended,
|
||||
type ISessionInfo,
|
||||
type IWithheld,
|
||||
MigrationState,
|
||||
type Mode,
|
||||
type SecretStorePrivateKeys,
|
||||
SESSION_BATCH_SIZE,
|
||||
type InboundGroupSessionData,
|
||||
type IRoomEncryption,
|
||||
} from "./base.ts";
|
||||
import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
|
||||
function encodeSessionKey(senderCurve25519Key: string, sessionId: string): string {
|
||||
return encodeURIComponent(senderCurve25519Key) + "/" + encodeURIComponent(sessionId);
|
||||
}
|
||||
|
||||
function decodeSessionKey(key: string): { senderKey: string; sessionId: string } {
|
||||
const keyParts = key.split("/");
|
||||
const senderKey = decodeURIComponent(keyParts[0]);
|
||||
const sessionId = decodeURIComponent(keyParts[1]);
|
||||
return { senderKey, sessionId };
|
||||
}
|
||||
CryptoStore,
|
||||
IDeviceData,
|
||||
IProblem,
|
||||
ISession,
|
||||
ISessionInfo,
|
||||
IWithheld,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
SecretStorePrivateKeys,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
|
||||
/**
|
||||
* Internal module. in-memory storage for e2e.
|
||||
*/
|
||||
|
||||
export class MemoryCryptoStore implements CryptoStore {
|
||||
private migrationState: MigrationState = MigrationState.NOT_STARTED;
|
||||
private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = [];
|
||||
private account: string | null = null;
|
||||
private crossSigningKeys: Record<string, CrossSigningKeyInfo> | null = null;
|
||||
private crossSigningKeys: Record<string, ICrossSigningKey> | null = null;
|
||||
private privateKeys: Partial<SecretStorePrivateKeys> = {};
|
||||
|
||||
private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {};
|
||||
private sessionProblems: { [deviceKey: string]: IProblem[] } = {};
|
||||
private notifiedErrorDevices: { [userId: string]: { [deviceId: string]: boolean } } = {};
|
||||
private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {};
|
||||
private inboundGroupSessionsWithheld: Record<string, IWithheld> = {};
|
||||
// Opaque device data object
|
||||
private deviceData: IDeviceData | null = null;
|
||||
private rooms: { [roomId: string]: IRoomEncryption } = {};
|
||||
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
|
||||
|
||||
/**
|
||||
* Returns true if this CryptoStore has ever been initialised (ie, it might contain data).
|
||||
*
|
||||
* Implementation of {@link CryptoStore.containsData}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async containsData(): Promise<boolean> {
|
||||
// If it contains anything, it should contain an account.
|
||||
return this.account !== null;
|
||||
}
|
||||
private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
|
||||
private parkedSharedHistory = new Map<string, ParkedSharedHistory[]>(); // keyed by room ID
|
||||
|
||||
/**
|
||||
* Ensure the database exists and is up-to-date.
|
||||
@@ -92,25 +78,186 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data on how much of the libolm to Rust Crypto migration has been done.
|
||||
* Look for an existing outgoing room key request, and if none is found,
|
||||
* add a new one
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getMigrationState}.
|
||||
*
|
||||
* @internal
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}: either the
|
||||
* same instance as passed in, or the existing one.
|
||||
*/
|
||||
public async getMigrationState(): Promise<MigrationState> {
|
||||
return this.migrationState;
|
||||
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
|
||||
const requestBody = request.requestBody;
|
||||
|
||||
return promiseTry(() => {
|
||||
// first see if we already have an entry for this request.
|
||||
const existing = this._getOutgoingRoomKeyRequest(requestBody);
|
||||
|
||||
if (existing) {
|
||||
// this entry matches the request - return it.
|
||||
logger.log(
|
||||
`already have key request outstanding for ` +
|
||||
`${requestBody.room_id} / ${requestBody.session_id}: ` +
|
||||
`not sending another`,
|
||||
);
|
||||
return existing;
|
||||
}
|
||||
|
||||
// we got to the end of the list without finding a match
|
||||
// - add the new request.
|
||||
logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id);
|
||||
this.outgoingRoomKeyRequests.push(request);
|
||||
return request;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data on how much of the libolm to Rust Crypto migration has been done.
|
||||
* Look for an existing room key request
|
||||
*
|
||||
* Implementation of {@link CryptoStore.setMigrationState}.
|
||||
* @param requestBody - existing request to look for
|
||||
*
|
||||
* @returns resolves to the matching
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* not found
|
||||
*/
|
||||
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody));
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for existing room key request, and returns the result synchronously.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param requestBody - existing request to look for
|
||||
*
|
||||
* @returns
|
||||
* the matching request, or null if not found
|
||||
*/
|
||||
public async setMigrationState(migrationState: MigrationState): Promise<void> {
|
||||
this.migrationState = migrationState;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private _getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): OutgoingRoomKeyRequest | null {
|
||||
for (const existing of this.outgoingRoomKeyRequests) {
|
||||
if (deepCompare(existing.requestBody, requestBody)) {
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by state
|
||||
*
|
||||
* @param wantedStates - list of acceptable states
|
||||
*
|
||||
* @returns resolves to the a
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* there are no pending requests in those states
|
||||
*/
|
||||
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
|
||||
for (const req of this.outgoingRoomKeyRequests) {
|
||||
for (const state of wantedStates) {
|
||||
if (req.state === state) {
|
||||
return Promise.resolve(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns All OutgoingRoomKeyRequests in state
|
||||
*/
|
||||
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
|
||||
return Promise.resolve(this.outgoingRoomKeyRequests.filter((r) => r.state == wantedState));
|
||||
}
|
||||
|
||||
public getOutgoingRoomKeyRequestsByTarget(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]> {
|
||||
const results: OutgoingRoomKeyRequest[] = [];
|
||||
|
||||
for (const req of this.outgoingRoomKeyRequests) {
|
||||
for (const state of wantedStates) {
|
||||
if (
|
||||
req.state === state &&
|
||||
req.recipients.some((recipient) => recipient.userId === userId && recipient.deviceId === deviceId)
|
||||
) {
|
||||
results.push(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and update it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
* @param updates - name/value map of updates to apply
|
||||
*
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}
|
||||
* updated request, or null if no matching row was found
|
||||
*/
|
||||
public updateOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
updates: Partial<OutgoingRoomKeyRequest>,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
for (const req of this.outgoingRoomKeyRequests) {
|
||||
if (req.requestId !== requestId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (req.state !== expectedState) {
|
||||
logger.warn(
|
||||
`Cannot update room key request from ${expectedState} ` +
|
||||
`as it was already updated to ${req.state}`,
|
||||
);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
Object.assign(req, updates);
|
||||
return Promise.resolve(req);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and delete it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
*
|
||||
* @returns resolves once the operation is completed
|
||||
*/
|
||||
public deleteOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
for (let i = 0; i < this.outgoingRoomKeyRequests.length; i++) {
|
||||
const req = this.outgoingRoomKeyRequests[i];
|
||||
|
||||
if (req.requestId !== requestId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (req.state != expectedState) {
|
||||
logger.warn(`Cannot delete room key request in state ${req.state} ` + `(expected ${expectedState})`);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
this.outgoingRoomKeyRequests.splice(i, 1);
|
||||
return Promise.resolve(req);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// Olm Account
|
||||
@@ -123,7 +270,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
this.account = accountPickle;
|
||||
}
|
||||
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void {
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
|
||||
func(this.crossSigningKeys);
|
||||
}
|
||||
|
||||
@@ -132,10 +279,14 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
func: (key: SecretStorePrivateKeys[K] | null) => void,
|
||||
type: K,
|
||||
): void {
|
||||
const result = this.privateKeys[type];
|
||||
const result = this.privateKeys[type] as SecretStorePrivateKeys[K] | undefined;
|
||||
func(result || null);
|
||||
}
|
||||
|
||||
public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
|
||||
this.crossSigningKeys = keys;
|
||||
}
|
||||
|
||||
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: unknown,
|
||||
type: K,
|
||||
@@ -147,11 +298,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
// Olm Sessions
|
||||
|
||||
public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
|
||||
let count = 0;
|
||||
for (const deviceSessions of Object.values(this.sessions)) {
|
||||
count += Object.keys(deviceSessions).length;
|
||||
}
|
||||
func(count);
|
||||
func(Object.keys(this.sessions).length);
|
||||
}
|
||||
|
||||
public getEndToEndSession(
|
||||
@@ -172,6 +319,18 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
func(this.sessions[deviceKey] || {});
|
||||
}
|
||||
|
||||
public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
|
||||
Object.entries(this.sessions).forEach(([deviceKey, deviceSessions]) => {
|
||||
Object.entries(deviceSessions).forEach(([sessionId, session]) => {
|
||||
func({
|
||||
...session,
|
||||
deviceKey,
|
||||
sessionId,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
|
||||
let deviceSessions = this.sessions[deviceKey];
|
||||
if (deviceSessions === undefined) {
|
||||
@@ -181,49 +340,50 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
safeSet(deviceSessions, sessionId, sessionInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getEndToEndSessionsBatch(): Promise<null | ISessionInfo[]> {
|
||||
const result: ISessionInfo[] = [];
|
||||
for (const deviceSessions of Object.values(this.sessions)) {
|
||||
for (const session of Object.values(deviceSessions)) {
|
||||
result.push(session);
|
||||
if (result.length >= SESSION_BATCH_SIZE) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
// No sessions left.
|
||||
return null;
|
||||
}
|
||||
|
||||
// There are fewer sessions than the batch size; return the final batch of sessions.
|
||||
return result;
|
||||
public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
|
||||
const problems = (this.sessionProblems[deviceKey] = this.sessionProblems[deviceKey] || []);
|
||||
problems.push({ type, fixed, time: Date.now() });
|
||||
problems.sort((a, b) => {
|
||||
return a.time - b.time;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise<void> {
|
||||
for (const { deviceKey, sessionId } of sessions) {
|
||||
const deviceSessions = this.sessions[deviceKey] || {};
|
||||
delete deviceSessions[sessionId];
|
||||
if (Object.keys(deviceSessions).length === 0) {
|
||||
// No more sessions for this device.
|
||||
delete this.sessions[deviceKey];
|
||||
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
|
||||
const problems = this.sessionProblems[deviceKey] || [];
|
||||
if (!problems.length) {
|
||||
return null;
|
||||
}
|
||||
const lastProblem = problems[problems.length - 1];
|
||||
for (const problem of problems) {
|
||||
if (problem.time > timestamp) {
|
||||
return Object.assign({}, problem, { fixed: lastProblem.fixed });
|
||||
}
|
||||
}
|
||||
if (lastProblem.fixed) {
|
||||
return null;
|
||||
} else {
|
||||
return lastProblem;
|
||||
}
|
||||
}
|
||||
|
||||
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
||||
const notifiedErrorDevices = this.notifiedErrorDevices;
|
||||
const ret: IOlmDevice[] = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const { userId, deviceInfo } = device;
|
||||
if (userId in notifiedErrorDevices) {
|
||||
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
|
||||
ret.push(device);
|
||||
safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
|
||||
}
|
||||
} else {
|
||||
ret.push(device);
|
||||
safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Inbound Group Sessions
|
||||
@@ -234,90 +394,136 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
txn: unknown,
|
||||
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
|
||||
): void {
|
||||
const k = encodeSessionKey(senderCurve25519Key, sessionId);
|
||||
const k = senderCurve25519Key + "/" + sessionId;
|
||||
func(this.inboundGroupSessions[k] || null, this.inboundGroupSessionsWithheld[k] || null);
|
||||
}
|
||||
|
||||
public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
|
||||
for (const key of Object.keys(this.inboundGroupSessions)) {
|
||||
// we can't use split, as the components we are trying to split out
|
||||
// might themselves contain '/' characters. We rely on the
|
||||
// senderKey being a (32-byte) curve25519 key, base64-encoded
|
||||
// (hence 43 characters long).
|
||||
|
||||
func({
|
||||
senderKey: key.slice(0, 43),
|
||||
sessionId: key.slice(44),
|
||||
sessionData: this.inboundGroupSessions[key],
|
||||
});
|
||||
}
|
||||
func(null);
|
||||
}
|
||||
|
||||
public addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: unknown,
|
||||
): void {
|
||||
const k = senderCurve25519Key + "/" + sessionId;
|
||||
if (this.inboundGroupSessions[k] === undefined) {
|
||||
this.inboundGroupSessions[k] = sessionData;
|
||||
}
|
||||
}
|
||||
|
||||
public storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: unknown,
|
||||
): void {
|
||||
const k = encodeSessionKey(senderCurve25519Key, sessionId);
|
||||
this.inboundGroupSessions[k] = sessionData;
|
||||
this.inboundGroupSessions[senderCurve25519Key + "/" + sessionId] = sessionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of Megolm sessions in the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.countEndToEndInboundGroupSessions}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async countEndToEndInboundGroupSessions(): Promise<number> {
|
||||
return Object.keys(this.inboundGroupSessions).length;
|
||||
public storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: IWithheld,
|
||||
txn: unknown,
|
||||
): void {
|
||||
const k = senderCurve25519Key + "/" + sessionId;
|
||||
this.inboundGroupSessionsWithheld[k] = sessionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getEndToEndInboundGroupSessionsBatch(): Promise<null | SessionExtended[]> {
|
||||
const result: SessionExtended[] = [];
|
||||
for (const [key, session] of Object.entries(this.inboundGroupSessions)) {
|
||||
result.push({
|
||||
...decodeSessionKey(key),
|
||||
sessionData: session,
|
||||
needsBackup: key in this.sessionsNeedingBackup,
|
||||
});
|
||||
if (result.length >= SESSION_BATCH_SIZE) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// Device Data
|
||||
|
||||
if (result.length === 0) {
|
||||
// No sessions left.
|
||||
return null;
|
||||
}
|
||||
|
||||
// There are fewer sessions than the batch size; return the final batch of sessions.
|
||||
return result;
|
||||
public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
|
||||
func(this.deviceData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async deleteEndToEndInboundGroupSessionsBatch(
|
||||
sessions: { senderKey: string; sessionId: string }[],
|
||||
): Promise<void> {
|
||||
for (const { senderKey, sessionId } of sessions) {
|
||||
const k = encodeSessionKey(senderKey, sessionId);
|
||||
delete this.inboundGroupSessions[k];
|
||||
}
|
||||
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
|
||||
this.deviceData = deviceData;
|
||||
}
|
||||
|
||||
// E2E rooms
|
||||
|
||||
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
|
||||
this.rooms[roomId] = roomInfo;
|
||||
}
|
||||
|
||||
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
|
||||
func(this.rooms);
|
||||
}
|
||||
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
const sessions: ISession[] = [];
|
||||
for (const session in this.sessionsNeedingBackup) {
|
||||
if (this.inboundGroupSessions[session]) {
|
||||
sessions.push({
|
||||
senderKey: session.slice(0, 43),
|
||||
sessionId: session.slice(44),
|
||||
sessionData: this.inboundGroupSessions[session],
|
||||
});
|
||||
if (limit && session.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(sessions);
|
||||
}
|
||||
|
||||
public countSessionsNeedingBackup(): Promise<number> {
|
||||
return Promise.resolve(Object.keys(this.sessionsNeedingBackup).length);
|
||||
}
|
||||
|
||||
public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
|
||||
for (const session of sessions) {
|
||||
const sessionKey = session.senderKey + "/" + session.sessionId;
|
||||
delete this.sessionsNeedingBackup[sessionKey];
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
|
||||
for (const session of sessions) {
|
||||
const sessionKey = encodeSessionKey(session.senderKey, session.sessionId);
|
||||
const sessionKey = session.senderKey + "/" + session.sessionId;
|
||||
this.sessionsNeedingBackup[sessionKey] = true;
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string): void {
|
||||
const sessions = this.sharedHistoryInboundGroupSessions[roomId] || [];
|
||||
sessions.push([senderKey, sessionId]);
|
||||
this.sharedHistoryInboundGroupSessions[roomId] = sessions;
|
||||
}
|
||||
|
||||
public getSharedHistoryInboundGroupSessions(roomId: string): Promise<[senderKey: string, sessionId: string][]> {
|
||||
return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
|
||||
}
|
||||
|
||||
public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory): void {
|
||||
const parked = this.parkedSharedHistory.get(roomId) ?? [];
|
||||
parked.push(parkedData);
|
||||
this.parkedSharedHistory.set(roomId, parked);
|
||||
}
|
||||
|
||||
public takeParkedSharedHistory(roomId: string): Promise<ParkedSharedHistory[]> {
|
||||
const parked = this.parkedSharedHistory.get(roomId) ?? [];
|
||||
this.parkedSharedHistory.delete(roomId);
|
||||
return Promise.resolve(parked);
|
||||
}
|
||||
|
||||
// Session key backups
|
||||
|
||||
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
|
||||
|
||||
34
node_modules/matrix-js-sdk/src/digest.ts
generated
vendored
34
node_modules/matrix-js-sdk/src/digest.ts
generated
vendored
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Computes a SHA-256 hash of a string (after utf-8 encoding) and returns it as an ArrayBuffer.
|
||||
*
|
||||
* @param plaintext The string to hash
|
||||
* @returns An Uint8Array containing the SHA-256 hash of the input string
|
||||
* @throws If the subtle crypto API is not available, for example if the code is running
|
||||
* in a web page with an insecure context (eg. served over plain HTTP).
|
||||
*/
|
||||
export async function sha256(plaintext: string): Promise<Uint8Array> {
|
||||
if (!globalThis.crypto.subtle) {
|
||||
throw new Error("Crypto.subtle is not available: insecure context?");
|
||||
}
|
||||
const utf8 = new TextEncoder().encode(plaintext);
|
||||
|
||||
const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8);
|
||||
|
||||
return new Uint8Array(digest);
|
||||
}
|
||||
683
node_modules/matrix-js-sdk/src/embedded.ts
generated
vendored
683
node_modules/matrix-js-sdk/src/embedded.ts
generated
vendored
@@ -15,51 +15,29 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type WidgetApi,
|
||||
WidgetApi,
|
||||
WidgetApiToWidgetAction,
|
||||
WidgetApiResponseError,
|
||||
MatrixCapabilities,
|
||||
type IWidgetApiRequest,
|
||||
type IWidgetApiAcknowledgeResponseData,
|
||||
type ISendEventToWidgetActionRequest,
|
||||
type ISendToDeviceToWidgetActionRequest,
|
||||
type ISendEventFromWidgetResponseData,
|
||||
type IWidgetApiRequestData,
|
||||
type WidgetApiAction,
|
||||
type IWidgetApiResponse,
|
||||
type IWidgetApiResponseData,
|
||||
type IUpdateStateToWidgetActionRequest,
|
||||
UnstableApiVersion,
|
||||
IWidgetApiRequest,
|
||||
IWidgetApiAcknowledgeResponseData,
|
||||
ISendEventToWidgetActionRequest,
|
||||
ISendToDeviceToWidgetActionRequest,
|
||||
ISendEventFromWidgetResponseData,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { type Transport } from "./matrixrtc/index.ts";
|
||||
import { MatrixEvent, type IEvent, EventStatus } from "./models/event.ts";
|
||||
import {
|
||||
type ISendEventResponse,
|
||||
type SendDelayedEventRequestOpts,
|
||||
type SendDelayedEventResponse,
|
||||
UpdateDelayedEventAction,
|
||||
isSendDelayedEventRequestOpts,
|
||||
} from "./@types/requests.ts";
|
||||
import { EventType, type StateEvents } from "./@types/event.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
import {
|
||||
MatrixClient,
|
||||
ClientEvent,
|
||||
type IMatrixClientCreateOpts,
|
||||
type IStartClientOpts,
|
||||
type SendToDeviceContentMap,
|
||||
type IOpenIDToken,
|
||||
UNSTABLE_MSC4140_DELAYED_EVENTS,
|
||||
} from "./client.ts";
|
||||
import { SyncApi, SyncState } from "./sync.ts";
|
||||
import { SlidingSyncSdk } from "./sliding-sync-sdk.ts";
|
||||
import { ConnectionError, MatrixError } from "./http-api/errors.ts";
|
||||
import { User } from "./models/user.ts";
|
||||
import { type Room } from "./models/room.ts";
|
||||
import { type ToDeviceBatch, type ToDevicePayload } from "./models/ToDeviceMessage.ts";
|
||||
import { MapWithDefault, type QueryDict, recursiveMapToObject } from "./utils.ts";
|
||||
import { type EmptyObject, TypedEventEmitter, UnsupportedDelayedEventsEndpointError } from "./matrix.ts";
|
||||
import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event";
|
||||
import { ISendEventResponse } from "./@types/requests";
|
||||
import { EventType } from "./@types/event";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts, SendToDeviceContentMap } from "./client";
|
||||
import { SyncApi, SyncState } from "./sync";
|
||||
import { SlidingSyncSdk } from "./sliding-sync-sdk";
|
||||
import { User } from "./models/user";
|
||||
import { Room } from "./models/room";
|
||||
import { ToDeviceBatch, ToDevicePayload } from "./models/ToDeviceMessage";
|
||||
import { DeviceInfo } from "./crypto/deviceinfo";
|
||||
import { IOlmDevice } from "./crypto/algorithms/megolm";
|
||||
import { MapWithDefault, recursiveMapToObject } from "./utils";
|
||||
|
||||
interface IStateEventRequest {
|
||||
eventType: string;
|
||||
@@ -110,48 +88,8 @@ export interface ICapabilities {
|
||||
* @defaultValue false
|
||||
*/
|
||||
turnServers?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to send delayed events.
|
||||
* @experimental Part of MSC4140 & MSC4157
|
||||
* @defaultValue false
|
||||
*/
|
||||
sendDelayedEvents?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to update delayed events.
|
||||
* @experimental Part of MSC4140 & MSC4157
|
||||
* @defaultValue false
|
||||
*/
|
||||
updateDelayedEvents?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to send sticky events.
|
||||
* @experimental Part of MSC4354 & MSC4407
|
||||
* @defaultValue false
|
||||
*/
|
||||
sendSticky?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to receive sticky events.
|
||||
* @experimental Part of MSC4354 & MSC4407
|
||||
* @defaultValue false
|
||||
*/
|
||||
receiveSticky?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to discover the RTC transports the host can reach.
|
||||
* @experimental Part of MSC4515
|
||||
* @defaultValue false
|
||||
*/
|
||||
rtcTransports?: boolean;
|
||||
}
|
||||
|
||||
export enum RoomWidgetClientEvent {
|
||||
PendingEventsChanged = "PendingEvent.pendingEventsChanged",
|
||||
}
|
||||
export type EventHandlerMap = { [RoomWidgetClientEvent.PendingEventsChanged]: () => void };
|
||||
|
||||
/**
|
||||
* A MatrixClient that routes its requests through the widget API instead of the
|
||||
* real CS API.
|
||||
@@ -160,83 +98,19 @@ export type EventHandlerMap = { [RoomWidgetClientEvent.PendingEventsChanged]: ()
|
||||
export class RoomWidgetClient extends MatrixClient {
|
||||
private room?: Room;
|
||||
private readonly widgetApiReady: Promise<void>;
|
||||
private readonly roomStateSynced: Promise<void>;
|
||||
private lifecycle?: AbortController;
|
||||
private syncState: SyncState | null = null;
|
||||
|
||||
private pendingSendingEventsTxId: { type: string; id: string | undefined; txId: string }[] = [];
|
||||
private eventEmitter = new TypedEventEmitter<keyof EventHandlerMap, EventHandlerMap>();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param widgetApi - The widget api to use for communication.
|
||||
* @param capabilities - The capabilities the widget client will request.
|
||||
* @param roomId - The room id the widget is associated with.
|
||||
* @param opts - The configuration options for this client.
|
||||
* @param sendContentLoaded - Whether to send a content loaded widget action immediately after initial setup.
|
||||
* Set to `false` if the widget uses `waitForIFrameLoad=true` (in this case the client does not expect a content loaded action at all),
|
||||
* or if the the widget wants to send the `ContentLoaded` action at a later point in time after the initial setup.
|
||||
*/
|
||||
public constructor(
|
||||
private readonly widgetApi: WidgetApi,
|
||||
private readonly capabilities: ICapabilities,
|
||||
private readonly roomId: string,
|
||||
opts: IMatrixClientCreateOpts,
|
||||
sendContentLoaded: boolean,
|
||||
) {
|
||||
super(opts);
|
||||
|
||||
const transportSend = this.widgetApi.transport.send.bind(this.widgetApi.transport);
|
||||
this.widgetApi.transport.send = async <
|
||||
T extends IWidgetApiRequestData,
|
||||
R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData,
|
||||
>(
|
||||
action: WidgetApiAction,
|
||||
data: T,
|
||||
): Promise<R> => {
|
||||
try {
|
||||
return await transportSend(action, data);
|
||||
} catch (error) {
|
||||
processAndThrow(error);
|
||||
}
|
||||
};
|
||||
|
||||
const transportSendComplete = this.widgetApi.transport.sendComplete.bind(this.widgetApi.transport);
|
||||
this.widgetApi.transport.sendComplete = async <T extends IWidgetApiRequestData, R extends IWidgetApiResponse>(
|
||||
action: WidgetApiAction,
|
||||
data: T,
|
||||
): Promise<R> => {
|
||||
try {
|
||||
return await transportSendComplete(action, data);
|
||||
} catch (error) {
|
||||
processAndThrow(error);
|
||||
}
|
||||
};
|
||||
|
||||
this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
|
||||
this.roomStateSynced = capabilities.receiveState?.length
|
||||
? new Promise<void>((resolve) =>
|
||||
this.widgetApi.once(`action:${WidgetApiToWidgetAction.UpdateState}`, resolve),
|
||||
)
|
||||
: Promise.resolve();
|
||||
|
||||
this.requestInitialCapabilities(capabilities, roomId);
|
||||
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
|
||||
|
||||
// Open communication with the host
|
||||
widgetApi.start();
|
||||
// Send a content loaded event now we've started the widget API
|
||||
// Note that element-web currently does not use waitForIFrameLoad=false and so
|
||||
// does *not* (yes, that is the right way around) wait for this event. Let's
|
||||
// start sending this, then once this has rolled out, we can change element-web to
|
||||
// use waitForIFrameLoad=false and have a widget API that's less racy.
|
||||
if (sendContentLoaded) widgetApi.sendContentLoaded();
|
||||
}
|
||||
|
||||
private requestInitialCapabilities(capabilities: ICapabilities, roomId: string): void {
|
||||
// Request capabilities for the functionality this client needs to support
|
||||
if (
|
||||
capabilities.sendEvent?.length ||
|
||||
@@ -248,61 +122,39 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
capabilities.sendState?.length ||
|
||||
capabilities.receiveState?.length
|
||||
) {
|
||||
this.widgetApi.requestCapabilityForRoomTimeline(roomId);
|
||||
widgetApi.requestCapabilityForRoomTimeline(roomId);
|
||||
}
|
||||
capabilities.sendEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToSendEvent(eventType));
|
||||
capabilities.receiveEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToReceiveEvent(eventType));
|
||||
capabilities.sendEvent?.forEach((eventType) => widgetApi.requestCapabilityToSendEvent(eventType));
|
||||
capabilities.receiveEvent?.forEach((eventType) => widgetApi.requestCapabilityToReceiveEvent(eventType));
|
||||
if (capabilities.sendMessage === true) {
|
||||
this.widgetApi.requestCapabilityToSendMessage();
|
||||
widgetApi.requestCapabilityToSendMessage();
|
||||
} else if (Array.isArray(capabilities.sendMessage)) {
|
||||
capabilities.sendMessage.forEach((msgType) => this.widgetApi.requestCapabilityToSendMessage(msgType));
|
||||
capabilities.sendMessage.forEach((msgType) => widgetApi.requestCapabilityToSendMessage(msgType));
|
||||
}
|
||||
if (capabilities.receiveMessage === true) {
|
||||
this.widgetApi.requestCapabilityToReceiveMessage();
|
||||
widgetApi.requestCapabilityToReceiveMessage();
|
||||
} else if (Array.isArray(capabilities.receiveMessage)) {
|
||||
capabilities.receiveMessage.forEach((msgType) => this.widgetApi.requestCapabilityToReceiveMessage(msgType));
|
||||
capabilities.receiveMessage.forEach((msgType) => widgetApi.requestCapabilityToReceiveMessage(msgType));
|
||||
}
|
||||
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
|
||||
this.widgetApi.requestCapabilityToSendState(eventType, stateKey),
|
||||
widgetApi.requestCapabilityToSendState(eventType, stateKey),
|
||||
);
|
||||
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
|
||||
this.widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
|
||||
widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
|
||||
);
|
||||
capabilities.sendToDevice?.forEach((eventType) => this.widgetApi.requestCapabilityToSendToDevice(eventType));
|
||||
capabilities.receiveToDevice?.forEach((eventType) =>
|
||||
this.widgetApi.requestCapabilityToReceiveToDevice(eventType),
|
||||
);
|
||||
if (
|
||||
capabilities.sendDelayedEvents &&
|
||||
(capabilities.sendEvent?.length ||
|
||||
capabilities.sendMessage === true ||
|
||||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
|
||||
capabilities.sendState?.length)
|
||||
) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||
}
|
||||
if (capabilities.updateDelayedEvents) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
}
|
||||
if (capabilities.sendSticky) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
}
|
||||
if (capabilities.receiveSticky) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4407ReceiveStickyEvent);
|
||||
}
|
||||
capabilities.sendToDevice?.forEach((eventType) => widgetApi.requestCapabilityToSendToDevice(eventType));
|
||||
capabilities.receiveToDevice?.forEach((eventType) => widgetApi.requestCapabilityToReceiveToDevice(eventType));
|
||||
if (capabilities.turnServers) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
|
||||
}
|
||||
if (capabilities.rtcTransports) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4515RtcTransports);
|
||||
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
|
||||
}
|
||||
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||
|
||||
// Open communication with the host
|
||||
widgetApi.start();
|
||||
}
|
||||
|
||||
public async supportUpdateState(): Promise<boolean> {
|
||||
return (await this.widgetApi.getClientVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE);
|
||||
}
|
||||
|
||||
private readonly syncApiResolver = Promise.withResolvers<void>();
|
||||
public async startClient(opts: IStartClientOpts = {}): Promise<void> {
|
||||
this.lifecycle = new AbortController();
|
||||
|
||||
@@ -321,49 +173,29 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
} else {
|
||||
this.syncApi = new SyncApi(this, opts, this.buildSyncApiOptions());
|
||||
}
|
||||
this.syncApiResolver.resolve();
|
||||
|
||||
this.room = this.syncApi.createRoom(this.roomId);
|
||||
this.store.storeRoom(this.room);
|
||||
|
||||
await this.widgetApiReady;
|
||||
|
||||
// sync room state:
|
||||
if (await this.supportUpdateState()) {
|
||||
// This will resolve once the client driver has sent us all the allowed room state.
|
||||
await this.roomStateSynced;
|
||||
} else {
|
||||
// Backfill the requested events
|
||||
// We only get the most recent event for every type + state key combo,
|
||||
// so it doesn't really matter what order we inject them in
|
||||
await Promise.all(
|
||||
this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => {
|
||||
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [
|
||||
this.roomId,
|
||||
]);
|
||||
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
|
||||
// Backfill the requested events
|
||||
// We only get the most recent event for every type + state key combo,
|
||||
// so it doesn't really matter what order we inject them in
|
||||
await Promise.all(
|
||||
this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => {
|
||||
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
|
||||
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
|
||||
|
||||
if (this.syncApi instanceof SyncApi) {
|
||||
// Passing events as `stateAfterEventList` will update the state.
|
||||
await this.syncApi.injectRoomEvents(this.room!, undefined, events);
|
||||
} else {
|
||||
await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync
|
||||
}
|
||||
events.forEach((event) => {
|
||||
this.emit(ClientEvent.Event, event);
|
||||
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
||||
});
|
||||
}) ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
this.cachedWellKnown.start(
|
||||
opts.clientWellKnownPollPeriod !== undefined ? 1000 * opts.clientWellKnownPollPeriod : undefined,
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], events);
|
||||
events.forEach((event) => {
|
||||
this.emit(ClientEvent.Event, event);
|
||||
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
||||
});
|
||||
}) ?? [],
|
||||
);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info("Finished initial sync");
|
||||
|
||||
this.matrixRTC.start();
|
||||
logger.info("Finished backfilling events");
|
||||
|
||||
// Watch for TURN servers, if requested
|
||||
if (this.capabilities.turnServers) this.watchTurnServers();
|
||||
@@ -372,7 +204,6 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
public stopClient(): void {
|
||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
|
||||
|
||||
super.stopClient();
|
||||
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
|
||||
@@ -383,87 +214,17 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
throw new Error(`Unknown room: ${roomIdOrAlias}`);
|
||||
}
|
||||
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
delayOpts: SendDelayedEventRequestOpts,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
delayOptsOrQuery?: SendDelayedEventRequestOpts | QueryDict,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse | SendDelayedEventResponse> {
|
||||
let queryOpts = queryDict;
|
||||
let delayOpts: SendDelayedEventRequestOpts | undefined;
|
||||
if (delayOptsOrQuery && isSendDelayedEventRequestOpts(delayOptsOrQuery)) {
|
||||
delayOpts = delayOptsOrQuery;
|
||||
} else if (!queryOpts) {
|
||||
queryOpts = delayOptsOrQuery;
|
||||
}
|
||||
|
||||
const stickyDurationMs = queryOpts?.["org.matrix.msc4354.sticky_duration_ms"];
|
||||
if (stickyDurationMs !== undefined && typeof stickyDurationMs !== "number") {
|
||||
throw new Error("Sticky duration must be a number when defined");
|
||||
}
|
||||
// This is save since we just checked that above
|
||||
// We need the additional as assertion for the EW linter to be happy.
|
||||
// It is not capable of implying the type based on the throw if `stickyDurationMs !== undefined && typeof stickyDurationMs !== "number"`
|
||||
// above
|
||||
const stickyDurationMsAsNumber: number | undefined = stickyDurationMs;
|
||||
|
||||
// We need to extend the content with the redacts parameter
|
||||
// The js sdk uses event.redacts but the widget api uses event.content.redacts
|
||||
// This will be converted back to event.redacts in the widget driver.
|
||||
const content = event.event.redacts
|
||||
? { ...event.getContent(), redacts: event.event.redacts }
|
||||
: event.getContent();
|
||||
|
||||
// Delayed event special case.
|
||||
if (delayOpts) {
|
||||
// TODO: updatePendingEvent for delayed events?
|
||||
const response = await this.widgetApi
|
||||
.sendRoomEvent(
|
||||
event.getType(),
|
||||
content,
|
||||
room.roomId,
|
||||
"delay" in delayOpts ? delayOpts.delay : undefined,
|
||||
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
|
||||
stickyDurationMsAsNumber,
|
||||
)
|
||||
.catch(timeoutToConnectionError);
|
||||
return this.validateSendDelayedEventResponse(response);
|
||||
}
|
||||
|
||||
const txId = event.getTxnId();
|
||||
// Add the txnId to the pending list (still with unknown evID)
|
||||
if (txId) this.pendingSendingEventsTxId.push({ type: event.getType(), id: undefined, txId });
|
||||
|
||||
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse> {
|
||||
let response: ISendEventFromWidgetResponseData;
|
||||
try {
|
||||
response = await this.widgetApi
|
||||
.sendRoomEvent(event.getType(), content, room.roomId, undefined, undefined, stickyDurationMsAsNumber)
|
||||
.catch(timeoutToConnectionError);
|
||||
response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId);
|
||||
} catch (e) {
|
||||
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
|
||||
throw e;
|
||||
}
|
||||
// This also checks for an event id on the response
|
||||
|
||||
room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
|
||||
|
||||
// Update the pending events list with the eventId
|
||||
this.pendingSendingEventsTxId.forEach((p) => {
|
||||
if (p.txId === txId) p.id = response.event_id;
|
||||
});
|
||||
this.eventEmitter.emit(RoomWidgetClientEvent.PendingEventsChanged);
|
||||
|
||||
return { event_id: response.event_id! };
|
||||
return { event_id: response.event_id };
|
||||
}
|
||||
|
||||
public async sendStateEvent(
|
||||
@@ -472,179 +233,14 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
content: any,
|
||||
stateKey = "",
|
||||
): Promise<ISendEventResponse> {
|
||||
const response = await this.widgetApi
|
||||
.sendStateEvent(eventType, stateKey, content, roomId)
|
||||
.catch(timeoutToConnectionError);
|
||||
if (response.event_id === undefined) {
|
||||
throw new Error("'event_id' absent from response to an event request");
|
||||
}
|
||||
return { event_id: response.event_id };
|
||||
return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
*/
|
||||
public async _unstable_sendDelayedStateEvent<K extends keyof StateEvents>(
|
||||
roomId: string,
|
||||
delayOpts: SendDelayedEventRequestOpts,
|
||||
eventType: K,
|
||||
content: StateEvents[K],
|
||||
stateKey = "",
|
||||
): Promise<SendDelayedEventResponse> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
"sendDelayedStateEvent",
|
||||
);
|
||||
}
|
||||
|
||||
const response = await this.widgetApi
|
||||
.sendStateEvent(
|
||||
eventType,
|
||||
stateKey,
|
||||
content,
|
||||
roomId,
|
||||
"delay" in delayOpts ? delayOpts.delay : undefined,
|
||||
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
|
||||
)
|
||||
.catch(timeoutToConnectionError);
|
||||
return this.validateSendDelayedEventResponse(response);
|
||||
}
|
||||
|
||||
private validateSendDelayedEventResponse(response: ISendEventFromWidgetResponseData): SendDelayedEventResponse {
|
||||
if (response.delay_id === undefined) {
|
||||
throw new Error("'delay_id' absent from response to a delayed event request");
|
||||
}
|
||||
return { delay_id: response.delay_id };
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
* @deprecated Instead use one of:
|
||||
* - {@link _unstable_cancelScheduledDelayedEvent}
|
||||
* - {@link _unstable_restartScheduledDelayedEvent}
|
||||
* - {@link _unstable_sendScheduledDelayedEvent}
|
||||
*/
|
||||
public async _unstable_updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
"updateDelayedEvent",
|
||||
);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case UpdateDelayedEventAction.Cancel:
|
||||
await this.widgetApi.cancelScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
break;
|
||||
case UpdateDelayedEventAction.Restart:
|
||||
await this.widgetApi.restartScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
break;
|
||||
case UpdateDelayedEventAction.Send:
|
||||
await this.widgetApi.sendScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
break;
|
||||
}
|
||||
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> {
|
||||
await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap));
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
*/
|
||||
public async _unstable_cancelScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
"cancelScheduledDelayedEvent",
|
||||
);
|
||||
}
|
||||
|
||||
await this.widgetApi.cancelScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
*/
|
||||
public async _unstable_restartScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
"restartScheduledDelayedEvent",
|
||||
);
|
||||
}
|
||||
|
||||
await this.widgetApi.restartScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
*/
|
||||
public async _unstable_sendScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
"sendScheduledDelayedEvent",
|
||||
);
|
||||
}
|
||||
|
||||
await this.widgetApi.sendScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* by {@link MatrixClient.encryptAndSendToDevice}.
|
||||
*/
|
||||
public async encryptAndSendToDevice(
|
||||
eventType: string,
|
||||
devices: { userId: string; deviceId: string }[],
|
||||
payload: ToDevicePayload,
|
||||
): Promise<void> {
|
||||
// map: user Id → device Id → payload
|
||||
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
|
||||
for (const { userId, deviceId } of devices) {
|
||||
contentMap.getOrCreate(userId).set(deviceId, payload);
|
||||
}
|
||||
|
||||
await this.widgetApi
|
||||
.sendToDevice(eventType, true, recursiveMapToObject(contentMap))
|
||||
.catch(timeoutToConnectionError);
|
||||
}
|
||||
|
||||
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<EmptyObject> {
|
||||
await this.widgetApi
|
||||
.sendToDevice(eventType, false, recursiveMapToObject(contentMap))
|
||||
.catch(timeoutToConnectionError);
|
||||
return {};
|
||||
}
|
||||
|
||||
public async getOpenIdToken(): Promise<IOpenIDToken> {
|
||||
const token = await this.widgetApi.requestOpenIDConnectToken().catch(timeoutToConnectionError);
|
||||
// the IOpenIDCredentials from the widget-api and IOpenIDToken form the matrix-js-sdk are compatible.
|
||||
// we still recreate the token to make this transparent and catch'able by the linter in case the types change in the future.
|
||||
return <IOpenIDToken>{
|
||||
access_token: token.access_token,
|
||||
expires_in: token.expires_in,
|
||||
matrix_server_name: token.matrix_server_name,
|
||||
token_type: token.token_type,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of configured RTC transports supported by the homeserver.
|
||||
*
|
||||
* Overrides the homeserver-side {@link MatrixClient._unstable_getRTCTransports} (MSC4143):
|
||||
* a widget cannot make authenticated homeserver calls itself, so we ask the host over the
|
||||
* widget API instead (MSC4515). Requires the `rtcTransports` capability and a host that
|
||||
* advertises the `org.matrix.msc4515` API version (otherwise the request throws).
|
||||
*/
|
||||
public override async _unstable_getRTCTransports(): Promise<Transport[]> {
|
||||
const { rtc_transports: rtcTransports } = await this.widgetApi
|
||||
.getRtcTransports()
|
||||
.catch(timeoutToConnectionError);
|
||||
return rtcTransports;
|
||||
}
|
||||
|
||||
public async queueToDevice({ eventType, batch }: ToDeviceBatch): Promise<void> {
|
||||
// map: user Id → device Id → payload
|
||||
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
|
||||
@@ -652,32 +248,20 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
contentMap.getOrCreate(userId).set(deviceId, payload);
|
||||
}
|
||||
|
||||
await this.widgetApi
|
||||
.sendToDevice(eventType, false, recursiveMapToObject(contentMap))
|
||||
.catch(timeoutToConnectionError);
|
||||
await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an event to a specific list of devices via the widget API. Optionally encrypts the event.
|
||||
*
|
||||
* If you are using a full MatrixClient you would be calling {@link MatrixClient.getCrypto().encryptToDeviceMessages()} followed
|
||||
* by {@link MatrixClient.queueToDevice}.
|
||||
*
|
||||
* However, this is combined into a single step when running as an embedded widget client. So, we expose this method for those
|
||||
* that need it.
|
||||
*
|
||||
* @param eventType - Type of the event to send.
|
||||
* @param encrypted - Whether the event should be encrypted.
|
||||
* @param contentMap - The content to send. Map from user_id to device_id to content object.
|
||||
*/
|
||||
public async sendToDeviceViaWidgetApi(
|
||||
eventType: string,
|
||||
encrypted: boolean,
|
||||
contentMap: SendToDeviceContentMap,
|
||||
): Promise<void> {
|
||||
await this.widgetApi
|
||||
.sendToDevice(eventType, encrypted, recursiveMapToObject(contentMap))
|
||||
.catch(timeoutToConnectionError);
|
||||
public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
|
||||
// map: user Id → device Id → payload
|
||||
const contentMap: MapWithDefault<string, Map<string, object>> = new MapWithDefault(() => new Map());
|
||||
for (const {
|
||||
userId,
|
||||
deviceInfo: { deviceId },
|
||||
} of userDeviceInfoArr) {
|
||||
contentMap.getOrCreate(userId).set(deviceId, payload);
|
||||
}
|
||||
|
||||
await this.widgetApi.sendToDevice((payload as { type: string }).type, true, recursiveMapToObject(contentMap));
|
||||
}
|
||||
|
||||
// Overridden since we get TURN servers automatically over the widget API,
|
||||
@@ -698,85 +282,20 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
}
|
||||
|
||||
private async ack(ev: CustomEvent<IWidgetApiRequest>): Promise<void> {
|
||||
this.widgetApi.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
|
||||
await this.widgetApi.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
|
||||
}
|
||||
|
||||
private updateTxId = async (event: MatrixEvent): Promise<void> => {
|
||||
// We update the txId for remote echos that originate from this client.
|
||||
// This happens with the help of `pendingSendingEventsTxId` where we store all events that are currently sending
|
||||
// with their widget txId and once ready the final evId.
|
||||
if (
|
||||
// This could theoretically be an event send by this device
|
||||
// In that case we need to update the txId of the event because the embedded client/widget
|
||||
// knows this event with a different transaction Id than what was used by the host client.
|
||||
event.getSender() === this.getUserId() &&
|
||||
// We optimize by not blocking events from types that we have not send
|
||||
// with this client.
|
||||
this.pendingSendingEventsTxId.some((p) => event.getType() === p.type)
|
||||
) {
|
||||
// Compare by event Id if we have a matching pending event where we know the txId.
|
||||
let matchingTxId = this.pendingSendingEventsTxId.find((p) => p.id === event.getId())?.txId;
|
||||
// Block any further processing of this event until we have received the sending response.
|
||||
// -> until we know the event id.
|
||||
// -> until we have not pending events anymore.
|
||||
while (!matchingTxId && this.pendingSendingEventsTxId.length > 0) {
|
||||
// Recheck whenever the PendingEventsChanged
|
||||
await new Promise<void>((resolve) =>
|
||||
this.eventEmitter.once(RoomWidgetClientEvent.PendingEventsChanged, () => resolve()),
|
||||
);
|
||||
matchingTxId = this.pendingSendingEventsTxId.find((p) => p.id === event.getId())?.txId;
|
||||
}
|
||||
|
||||
// We found the correct txId: we update the event and delete the entry of the pending events.
|
||||
if (matchingTxId) {
|
||||
event.setTxnId(matchingTxId);
|
||||
event.setUnsigned({ ...event.getUnsigned(), transaction_id: matchingTxId });
|
||||
}
|
||||
this.pendingSendingEventsTxId = this.pendingSendingEventsTxId.filter((p) => p.id !== event.getId());
|
||||
|
||||
// Emit once there are no pending events anymore to release all other events that got
|
||||
// awaited in the `while (!matchingTxId && this.pendingSendingEventsTxId.length > 0)` loop
|
||||
// but are not send by this client.
|
||||
if (this.pendingSendingEventsTxId.length === 0) {
|
||||
this.eventEmitter.emit(RoomWidgetClientEvent.PendingEventsChanged);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onEvent = async (ev: CustomEvent<ISendEventToWidgetActionRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
|
||||
// Verify the room ID matches, since it's possible for the client to
|
||||
// send us events from other rooms if this widget is always on screen
|
||||
if (ev.detail.data.room_id === this.roomId) {
|
||||
const event = new MatrixEvent(ev.detail.data);
|
||||
|
||||
// Only inject once we have update the txId
|
||||
await this.updateTxId(event);
|
||||
await this.syncApiResolver.promise;
|
||||
if (this.syncApi instanceof SyncApi) {
|
||||
if (await this.supportUpdateState()) {
|
||||
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
|
||||
} else {
|
||||
// Passing undefined for `stateAfterEventList` will make `injectRoomEvents` run in legacy mode
|
||||
// -> state events in `timelineEventList` will update the state.
|
||||
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
|
||||
}
|
||||
} else {
|
||||
// Sliding Sync
|
||||
if (await this.supportUpdateState()) {
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
|
||||
} else {
|
||||
logger.error(
|
||||
"slididng sync cannot be used in widget mode if the client widget driver does not support the version: 'org.matrix.msc2762_update_state'",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const event = new MatrixEvent(ev.detail.data as Partial<IEvent>);
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
|
||||
this.emit(ClientEvent.Event, event);
|
||||
if (event.unstableStickyInfo !== undefined) this.room!._unstable_addStickyEvents([event]);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info(`Received event ${event.getId()} ${event.getType()}`);
|
||||
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
||||
} else {
|
||||
const { event_id: eventId, room_id: roomId } = ev.detail.data;
|
||||
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
|
||||
@@ -791,7 +310,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
const event = new MatrixEvent({
|
||||
type: ev.detail.data.type,
|
||||
sender: ev.detail.data.sender,
|
||||
content: ev.detail.data.content,
|
||||
content: ev.detail.data.content as IContent,
|
||||
});
|
||||
// Mark the event as encrypted if it was, using fake contents and keys since those are unknown to us
|
||||
if (ev.detail.data.encrypted) event.makeEncrypted(EventType.RoomMessageEncrypted, {}, "", "");
|
||||
@@ -801,37 +320,6 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
await this.ack(ev);
|
||||
};
|
||||
|
||||
private onStateUpdate = async (ev: CustomEvent<IUpdateStateToWidgetActionRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
if (!(await this.supportUpdateState())) {
|
||||
logger.warn(
|
||||
"received update_state widget action but the widget driver did not claim to support 'org.matrix.msc2762_update_state'",
|
||||
);
|
||||
}
|
||||
await this.syncApiResolver.promise;
|
||||
for (const rawEvent of ev.detail.data.state) {
|
||||
// Verify the room ID matches, since it's possible for the client to
|
||||
// send us state updates from other rooms if this widget is always
|
||||
// on screen
|
||||
if (rawEvent.room_id === this.roomId) {
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
|
||||
if (this.syncApi instanceof SyncApi) {
|
||||
await this.syncApi.injectRoomEvents(this.room!, undefined, [event]);
|
||||
} else {
|
||||
// Sliding Sync
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [event]);
|
||||
}
|
||||
logger.debug(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`);
|
||||
} else {
|
||||
const { event_id: eventId, room_id: roomId } = ev.detail.data;
|
||||
logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.ack(ev);
|
||||
};
|
||||
|
||||
private async watchTurnServers(): Promise<void> {
|
||||
const servers = this.widgetApi.getTurnServers();
|
||||
const onClientStopped = (): void => {
|
||||
@@ -858,24 +346,3 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processAndThrow(error: unknown): never {
|
||||
if (error instanceof WidgetApiResponseError && error.data.matrix_api_error) {
|
||||
throw MatrixError.fromWidgetApiErrorData(error.data.matrix_api_error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This converts an "Request timed out" error from the PostmessageTransport into a ConnectionError.
|
||||
* It either throws the original error or a new ConnectionError.
|
||||
**/
|
||||
function timeoutToConnectionError(error: unknown): never {
|
||||
// TODO: this should not check on error.message but instead it should be a specific type
|
||||
// error instanceof WidgetTimeoutError
|
||||
if (error instanceof Error && error.message === "Request timed out") {
|
||||
throw new ConnectionError("widget api timeout");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
68
node_modules/matrix-js-sdk/src/errors.ts
generated
vendored
68
node_modules/matrix-js-sdk/src/errors.ts
generated
vendored
@@ -14,6 +14,22 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export enum InvalidStoreState {
|
||||
ToggledLazyLoading,
|
||||
}
|
||||
|
||||
export class InvalidStoreError extends Error {
|
||||
public static TOGGLED_LAZY_LOADING = InvalidStoreState.ToggledLazyLoading;
|
||||
|
||||
public constructor(public readonly reason: InvalidStoreState, public readonly value: any) {
|
||||
const message =
|
||||
`Store is invalid because ${reason}, ` +
|
||||
`please stop the client, delete all data and start the client again`;
|
||||
super(message);
|
||||
this.name = "InvalidStoreError";
|
||||
}
|
||||
}
|
||||
|
||||
export enum InvalidCryptoStoreState {
|
||||
TooNew = "TOO_NEW",
|
||||
}
|
||||
@@ -31,57 +47,7 @@ export class InvalidCryptoStoreError extends Error {
|
||||
}
|
||||
|
||||
export class KeySignatureUploadError extends Error {
|
||||
public constructor(
|
||||
message: string,
|
||||
public readonly value: any,
|
||||
) {
|
||||
public constructor(message: string, public readonly value: any) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It is invalid to call most methods once {@link MatrixClient#stopClient} has been called.
|
||||
*
|
||||
* This error will be thrown if you attempt to do so.
|
||||
*
|
||||
* {@link MatrixClient#stopClient} itself is an exception to this: it may safely be called multiple times on the same
|
||||
* instance.
|
||||
*/
|
||||
export class ClientStoppedError extends Error {
|
||||
public constructor() {
|
||||
super("MatrixClient has been stopped");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This error is thrown when the Homeserver does not support the delayed events endpoints.
|
||||
*/
|
||||
export class UnsupportedDelayedEventsEndpointError extends Error {
|
||||
public constructor(
|
||||
message: string,
|
||||
public clientEndpoint:
|
||||
| "sendDelayedEvent"
|
||||
| "updateDelayedEvent"
|
||||
| "cancelScheduledDelayedEvent"
|
||||
| "restartScheduledDelayedEvent"
|
||||
| "sendScheduledDelayedEvent"
|
||||
| "sendDelayedStateEvent"
|
||||
| "getDelayedEvents",
|
||||
) {
|
||||
super(message);
|
||||
this.name = "UnsupportedDelayedEventsEndpointError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This error is thrown when the Homeserver does not support the sticky events endpoints.
|
||||
*/
|
||||
export class UnsupportedStickyEventsEndpointError extends Error {
|
||||
public constructor(
|
||||
message: string,
|
||||
public clientEndpoint: "sendStickyEvent" | "sendStickyStateEvent",
|
||||
) {
|
||||
super(message);
|
||||
this.name = "UnsupportedStickyEventsEndpointError";
|
||||
}
|
||||
}
|
||||
|
||||
15
node_modules/matrix-js-sdk/src/event-mapper.ts
generated
vendored
15
node_modules/matrix-js-sdk/src/event-mapper.ts
generated
vendored
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MatrixClient } from "./client.ts";
|
||||
import { type IEvent, MatrixEvent, MatrixEventEvent } from "./models/event.ts";
|
||||
import { RelationType } from "./@types/event.ts";
|
||||
import { MatrixClient } from "./client";
|
||||
import { IEvent, MatrixEvent, MatrixEventEvent } from "./models/event";
|
||||
import { RelationType } from "./@types/event";
|
||||
|
||||
export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface MapperOpts {
|
||||
preventReEmit?: boolean;
|
||||
// decrypt event proactively
|
||||
decrypt?: boolean;
|
||||
// the event is a to_device event
|
||||
toDevice?: boolean;
|
||||
}
|
||||
|
||||
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
|
||||
@@ -32,6 +34,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
const decrypt = options.decrypt !== false;
|
||||
|
||||
function mapper(plainOldJsObject: Partial<IEvent>): MatrixEvent {
|
||||
if (options.toDevice) {
|
||||
delete plainOldJsObject.room_id;
|
||||
}
|
||||
|
||||
const room = client.getRoom(plainOldJsObject.room_id);
|
||||
|
||||
let event: MatrixEvent | undefined;
|
||||
@@ -68,6 +74,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
event.setThread(thread);
|
||||
}
|
||||
|
||||
// TODO: once we get rid of the old libolm-backed crypto, we can restrict this to room events (rather than
|
||||
// to-device events), because the rust implementation decrypts to-device messages at a higher level.
|
||||
// Generally we probably want to use a different eventMapper implementation for to-device events because
|
||||
if (event.isEncrypted()) {
|
||||
if (!preventReEmit) {
|
||||
client.reEmitter.reEmit(event, [MatrixEventEvent.Decrypted]);
|
||||
|
||||
2
node_modules/matrix-js-sdk/src/extensible_events_v1/ExtensibleEvent.ts
generated
vendored
2
node_modules/matrix-js-sdk/src/extensible_events_v1/ExtensibleEvent.ts
generated
vendored
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ExtensibleEventType, type IPartialEvent } from "../@types/extensible_events.ts";
|
||||
import { ExtensibleEventType, IPartialEvent } from "../@types/extensible_events";
|
||||
|
||||
/**
|
||||
* Represents an Extensible Event in Matrix.
|
||||
|
||||
22
node_modules/matrix-js-sdk/src/extensible_events_v1/MessageEvent.ts
generated
vendored
22
node_modules/matrix-js-sdk/src/extensible_events_v1/MessageEvent.ts
generated
vendored
@@ -14,19 +14,21 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ExtensibleEvent } from "./ExtensibleEvent.ts";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
|
||||
import { ExtensibleEvent } from "./ExtensibleEvent";
|
||||
import {
|
||||
type ExtensibleEventType,
|
||||
type IMessageRendering,
|
||||
type IPartialEvent,
|
||||
ExtensibleEventType,
|
||||
IMessageRendering,
|
||||
IPartialEvent,
|
||||
isEventTypeSame,
|
||||
M_HTML,
|
||||
M_MESSAGE,
|
||||
type ExtensibleAnyMessageEventContent,
|
||||
ExtensibleAnyMessageEventContent,
|
||||
M_TEXT,
|
||||
} from "../@types/extensible_events.ts";
|
||||
import { isOptionalAString, isProvided } from "./utilities.ts";
|
||||
import { InvalidEventError } from "./InvalidEventError.ts";
|
||||
} from "../@types/extensible_events";
|
||||
import { isOptionalAString, isProvided } from "./utilities";
|
||||
import { InvalidEventError } from "./InvalidEventError";
|
||||
|
||||
/**
|
||||
* Represents a message event. Message events are the simplest form of event with
|
||||
@@ -44,7 +46,7 @@ export class MessageEvent extends ExtensibleEvent<ExtensibleAnyMessageEventConte
|
||||
/**
|
||||
* The default HTML for the event, if provided.
|
||||
*/
|
||||
public readonly html?: string;
|
||||
public readonly html: Optional<string>;
|
||||
|
||||
/**
|
||||
* All the different renderings of the message. Note that this is the same
|
||||
@@ -80,7 +82,7 @@ export class MessageEvent extends ExtensibleEvent<ExtensibleAnyMessageEventConte
|
||||
this.renderings = mmessage;
|
||||
} else if (isOptionalAString(mtext)) {
|
||||
this.text = mtext;
|
||||
this.html = mhtml ?? undefined;
|
||||
this.html = mhtml;
|
||||
this.renderings = [{ body: mtext, mimetype: "text/plain" }];
|
||||
if (this.html) {
|
||||
this.renderings.push({ body: this.html, mimetype: "text/html" });
|
||||
|
||||
14
node_modules/matrix-js-sdk/src/extensible_events_v1/PollEndEvent.ts
generated
vendored
14
node_modules/matrix-js-sdk/src/extensible_events_v1/PollEndEvent.ts
generated
vendored
@@ -15,16 +15,16 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type ExtensibleEventType,
|
||||
type IPartialEvent,
|
||||
ExtensibleEventType,
|
||||
IPartialEvent,
|
||||
isEventTypeSame,
|
||||
M_TEXT,
|
||||
REFERENCE_RELATION,
|
||||
} from "../@types/extensible_events.ts";
|
||||
import { M_POLL_END, type PollEndEventContent } from "../@types/polls.ts";
|
||||
import { ExtensibleEvent } from "./ExtensibleEvent.ts";
|
||||
import { InvalidEventError } from "./InvalidEventError.ts";
|
||||
import { MessageEvent } from "./MessageEvent.ts";
|
||||
} from "../@types/extensible_events";
|
||||
import { M_POLL_END, PollEndEventContent } from "../@types/polls";
|
||||
import { ExtensibleEvent } from "./ExtensibleEvent";
|
||||
import { InvalidEventError } from "./InvalidEventError";
|
||||
import { MessageEvent } from "./MessageEvent";
|
||||
|
||||
/**
|
||||
* Represents a poll end/closure event.
|
||||
|
||||
15
node_modules/matrix-js-sdk/src/extensible_events_v1/PollResponseEvent.ts
generated
vendored
15
node_modules/matrix-js-sdk/src/extensible_events_v1/PollResponseEvent.ts
generated
vendored
@@ -14,16 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ExtensibleEvent } from "./ExtensibleEvent.ts";
|
||||
import { M_POLL_RESPONSE, type PollResponseEventContent, type PollResponseSubtype } from "../@types/polls.ts";
|
||||
import {
|
||||
type ExtensibleEventType,
|
||||
type IPartialEvent,
|
||||
isEventTypeSame,
|
||||
REFERENCE_RELATION,
|
||||
} from "../@types/extensible_events.ts";
|
||||
import { InvalidEventError } from "./InvalidEventError.ts";
|
||||
import { type PollStartEvent } from "./PollStartEvent.ts";
|
||||
import { ExtensibleEvent } from "./ExtensibleEvent";
|
||||
import { M_POLL_RESPONSE, PollResponseEventContent, PollResponseSubtype } from "../@types/polls";
|
||||
import { ExtensibleEventType, IPartialEvent, isEventTypeSame, REFERENCE_RELATION } from "../@types/extensible_events";
|
||||
import { InvalidEventError } from "./InvalidEventError";
|
||||
import { PollStartEvent } from "./PollStartEvent";
|
||||
|
||||
/**
|
||||
* Represents a poll response event.
|
||||
|
||||
18
node_modules/matrix-js-sdk/src/extensible_events_v1/PollStartEvent.ts
generated
vendored
18
node_modules/matrix-js-sdk/src/extensible_events_v1/PollStartEvent.ts
generated
vendored
@@ -16,19 +16,19 @@ limitations under the License.
|
||||
|
||||
import { NamespacedValue } from "matrix-events-sdk";
|
||||
|
||||
import { MessageEvent } from "./MessageEvent.ts";
|
||||
import { type ExtensibleEventType, type IPartialEvent, isEventTypeSame, M_TEXT } from "../@types/extensible_events.ts";
|
||||
import { MessageEvent } from "./MessageEvent";
|
||||
import { ExtensibleEventType, IPartialEvent, isEventTypeSame, M_TEXT } from "../@types/extensible_events";
|
||||
import {
|
||||
type KnownPollKind,
|
||||
KnownPollKind,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
M_POLL_KIND_UNDISCLOSED,
|
||||
M_POLL_START,
|
||||
type PollStartEventContent,
|
||||
type PollStartSubtype,
|
||||
type PollAnswer,
|
||||
} from "../@types/polls.ts";
|
||||
import { InvalidEventError } from "./InvalidEventError.ts";
|
||||
import { ExtensibleEvent } from "./ExtensibleEvent.ts";
|
||||
PollStartEventContent,
|
||||
PollStartSubtype,
|
||||
PollAnswer,
|
||||
} from "../@types/polls";
|
||||
import { InvalidEventError } from "./InvalidEventError";
|
||||
import { ExtensibleEvent } from "./ExtensibleEvent";
|
||||
|
||||
/**
|
||||
* Represents a poll answer. Note that this is represented as a subtype and is
|
||||
|
||||
4
node_modules/matrix-js-sdk/src/extensible_events_v1/utilities.ts
generated
vendored
4
node_modules/matrix-js-sdk/src/extensible_events_v1/utilities.ts
generated
vendored
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Optional } from "matrix-events-sdk";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
|
||||
/**
|
||||
* Determines if the given optional was provided a value.
|
||||
* @param s - The optional to test.
|
||||
* @returns True if the value is defined.
|
||||
*/
|
||||
export function isProvided<T>(s: Optional<T>): s is T {
|
||||
export function isProvided<T>(s: Optional<T>): boolean {
|
||||
return s !== null && s !== undefined;
|
||||
}
|
||||
|
||||
|
||||
6
node_modules/matrix-js-sdk/src/feature.ts
generated
vendored
6
node_modules/matrix-js-sdk/src/feature.ts
generated
vendored
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type IServerVersions } from "./client.ts";
|
||||
import { IServerVersions } from "./client";
|
||||
|
||||
export enum ServerSupport {
|
||||
Stable,
|
||||
@@ -55,9 +55,11 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
|
||||
[Feature.RelationBasedRedactions]: {
|
||||
unstablePrefixes: ["org.matrix.msc3912"],
|
||||
},
|
||||
[Feature.AccountDataDeletion]: {
|
||||
unstablePrefixes: ["org.matrix.msc3391"],
|
||||
},
|
||||
[Feature.RelationsRecursion]: {
|
||||
unstablePrefixes: ["org.matrix.msc3981"],
|
||||
matrixVersion: "v1.10",
|
||||
},
|
||||
[Feature.IntentionalMentions]: {
|
||||
unstablePrefixes: ["org.matrix.msc3952_intentional_mentions"],
|
||||
|
||||
39
node_modules/matrix-js-sdk/src/filter-component.ts
generated
vendored
39
node_modules/matrix-js-sdk/src/filter-component.ts
generated
vendored
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type RelationType } from "./@types/event.ts";
|
||||
import { type MatrixEvent } from "./models/event.ts";
|
||||
import { FILTER_RELATED_BY_REL_TYPES, FILTER_RELATED_BY_SENDERS, THREAD_RELATION_TYPE } from "./models/thread.ts";
|
||||
import { RelationType } from "./@types/event";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { FILTER_RELATED_BY_REL_TYPES, FILTER_RELATED_BY_SENDERS, THREAD_RELATION_TYPE } from "./models/thread";
|
||||
|
||||
/**
|
||||
* Checks if a value matches a given field value, which may be a * terminated
|
||||
@@ -34,6 +34,7 @@ function matchesWildcard(actualValue: string, filterValue: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IFilterComponent {
|
||||
"types"?: string[];
|
||||
"not_types"?: string[];
|
||||
@@ -50,6 +51,7 @@ export interface IFilterComponent {
|
||||
"io.element.relation_senders"?: Array<RelationType | string>;
|
||||
"io.element.relation_types"?: string[];
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
/**
|
||||
* FilterComponent is a section of a Filter definition which defines the
|
||||
@@ -60,10 +62,7 @@ export interface IFilterComponent {
|
||||
* 'Filters' are referred to as 'FilterCollections'.
|
||||
*/
|
||||
export class FilterComponent {
|
||||
public constructor(
|
||||
private filterJson: IFilterComponent,
|
||||
public readonly userId?: string | undefined | null,
|
||||
) {}
|
||||
public constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {}
|
||||
|
||||
/**
|
||||
* Checks with the filter component matches the given event
|
||||
@@ -97,19 +96,17 @@ export class FilterComponent {
|
||||
* Converts the filter component into the form expected over the wire
|
||||
*/
|
||||
public toJSON(): object {
|
||||
return Object.fromEntries(
|
||||
Object.entries({
|
||||
types: this.filterJson.types,
|
||||
not_types: this.filterJson.not_types,
|
||||
rooms: this.filterJson.rooms,
|
||||
not_rooms: this.filterJson.not_rooms,
|
||||
senders: this.filterJson.senders,
|
||||
not_senders: this.filterJson.not_senders,
|
||||
contains_url: this.filterJson.contains_url,
|
||||
[FILTER_RELATED_BY_SENDERS.name]: this.filterJson[FILTER_RELATED_BY_SENDERS.name],
|
||||
[FILTER_RELATED_BY_REL_TYPES.name]: this.filterJson[FILTER_RELATED_BY_REL_TYPES.name],
|
||||
}).filter(([_key, value]) => value),
|
||||
);
|
||||
return {
|
||||
types: this.filterJson.types || null,
|
||||
not_types: this.filterJson.not_types || [],
|
||||
rooms: this.filterJson.rooms || null,
|
||||
not_rooms: this.filterJson.not_rooms || [],
|
||||
senders: this.filterJson.senders || null,
|
||||
not_senders: this.filterJson.not_senders || [],
|
||||
contains_url: this.filterJson.contains_url || null,
|
||||
[FILTER_RELATED_BY_SENDERS.name]: this.filterJson[FILTER_RELATED_BY_SENDERS.name] || [],
|
||||
[FILTER_RELATED_BY_REL_TYPES.name]: this.filterJson[FILTER_RELATED_BY_REL_TYPES.name] || [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,7 +139,6 @@ export class FilterComponent {
|
||||
},
|
||||
} as const;
|
||||
|
||||
// oxlint-disable-next-line guard-for-in
|
||||
for (const name in literalKeys) {
|
||||
const matchFunc = literalKeys[<keyof typeof literalKeys>name];
|
||||
const notName = "not_" + name;
|
||||
@@ -194,7 +190,6 @@ export class FilterComponent {
|
||||
* @returns events which matched the filter component
|
||||
*/
|
||||
public filter(events: MatrixEvent[]): MatrixEvent[] {
|
||||
// oxlint-disable-next-line typescript/unbound-method
|
||||
return events.filter(this.check, this);
|
||||
}
|
||||
|
||||
|
||||
50
node_modules/matrix-js-sdk/src/filter.ts
generated
vendored
50
node_modules/matrix-js-sdk/src/filter.ts
generated
vendored
@@ -14,13 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type EventType, type RelationType } from "./@types/event.ts";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync.ts";
|
||||
import { FilterComponent, type IFilterComponent } from "./filter-component.ts";
|
||||
import { type MatrixEvent } from "./models/event.ts";
|
||||
import { NamespacedValue } from "./NamespacedValue.ts";
|
||||
|
||||
const profileFieldsFilterName = new NamespacedValue("profile_fields", "org.matrix.msc4429.profile_fields");
|
||||
import { EventType, RelationType } from "./@types/event";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
|
||||
import { FilterComponent, IFilterComponent } from "./filter-component";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
|
||||
/**
|
||||
*/
|
||||
@@ -36,14 +33,13 @@ function setProp(obj: Record<string, any>, keyNesting: string, val: any): void {
|
||||
currentObj[nestedKeys[nestedKeys.length - 1]] = val;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IFilterDefinition {
|
||||
"event_fields"?: string[];
|
||||
"event_format"?: "client" | "federation";
|
||||
"presence"?: IFilterComponent;
|
||||
"account_data"?: IFilterComponent;
|
||||
"room"?: IRoomFilter;
|
||||
"profile_fields"?: ProfileFieldsFilter;
|
||||
"org.matrix.msc4429.profile_fields"?: ProfileFieldsFilter;
|
||||
event_fields?: string[];
|
||||
event_format?: "client" | "federation";
|
||||
presence?: IFilterComponent;
|
||||
account_data?: IFilterComponent;
|
||||
room?: IRoomFilter;
|
||||
}
|
||||
|
||||
export interface IRoomEventFilter extends IFilterComponent {
|
||||
@@ -71,13 +67,7 @@ interface IRoomFilter {
|
||||
timeline?: IRoomEventFilter;
|
||||
account_data?: IRoomEventFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter section used for requesting a set of extended profile fields that will be sent down the sync stream.
|
||||
*/
|
||||
interface ProfileFieldsFilter {
|
||||
ids: string[];
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
export class Filter {
|
||||
public static LAZY_LOADING_MESSAGES_FILTER = {
|
||||
@@ -102,10 +92,7 @@ export class Filter {
|
||||
* @param userId - The user ID for this filter.
|
||||
* @param filterId - The filter ID if known.
|
||||
*/
|
||||
public constructor(
|
||||
public readonly userId: string | undefined | null,
|
||||
public filterId?: string,
|
||||
) {}
|
||||
public constructor(public readonly userId: string | undefined | null, public filterId?: string) {}
|
||||
|
||||
/**
|
||||
* Get the ID of this filter on your homeserver (if known)
|
||||
@@ -252,17 +239,4 @@ export class Filter {
|
||||
public setIncludeLeaveRooms(includeLeave: boolean): void {
|
||||
setProp(this.definition, "room.include_leave", includeLeave);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of fields to be included in the profile information sent down the sync stream.
|
||||
* @param ids The field IDs to sync.
|
||||
* @param stable Whether to use the stable or unstable versions of this filter.
|
||||
* @experimental
|
||||
*/
|
||||
public setUnstableMSC4429SyncUserProfiles(ids: string[], stable: boolean): void {
|
||||
const field = stable
|
||||
? profileFieldsFilterName.name
|
||||
: (profileFieldsFilterName.unstable ?? profileFieldsFilterName.name);
|
||||
this.definition[field] = { ids };
|
||||
}
|
||||
}
|
||||
|
||||
205
node_modules/matrix-js-sdk/src/http-api/errors.ts
generated
vendored
205
node_modules/matrix-js-sdk/src/http-api/errors.ts
generated
vendored
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 - 2024 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
@@ -14,12 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type IMatrixApiError as IWidgetMatrixError } from "matrix-widget-api";
|
||||
|
||||
import { type IUsageLimit } from "../@types/partials.ts";
|
||||
import { type MatrixEvent } from "../models/event.ts";
|
||||
import { NamespacedValue } from "../NamespacedValue.ts";
|
||||
import { hasRequiredStringProperty, isRecord } from "../@types/type-guards.ts";
|
||||
import { IUsageLimit } from "../@types/partials";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
|
||||
interface IErrorJson extends Partial<IUsageLimit> {
|
||||
[key: string]: any; // extensible
|
||||
@@ -32,75 +28,16 @@ interface IErrorJson extends Partial<IUsageLimit> {
|
||||
* specific to HTTP responses.
|
||||
* @param msg - The error message to include.
|
||||
* @param httpStatus - The HTTP response status code.
|
||||
* @param httpHeaders - The HTTP response headers.
|
||||
*/
|
||||
export class HTTPError extends Error {
|
||||
public constructor(
|
||||
msg: string,
|
||||
public readonly httpStatus?: number,
|
||||
public readonly httpHeaders?: Headers,
|
||||
) {
|
||||
public constructor(msg: string, public readonly httpStatus?: number) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this error was due to rate-limiting on the server side (and should therefore be retried after a delay).
|
||||
*
|
||||
* If this returns `true`, {@link getRetryAfterMs} can be called to retrieve the server-side
|
||||
* recommendation for the retry period.
|
||||
*
|
||||
* @returns Whether this error is due to rate-limiting.
|
||||
*/
|
||||
public isRateLimitError(): boolean {
|
||||
return this.httpStatus === 429;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The recommended delay in milliseconds to wait before retrying
|
||||
* the request that triggered this error, or null if no delay is recommended.
|
||||
* @throws Error if the recommended delay is an invalid value.
|
||||
* @see {@link safeGetRetryAfterMs} for a version of this check that doesn't throw.
|
||||
*/
|
||||
public getRetryAfterMs(): number | null {
|
||||
const retryAfter = this.httpHeaders?.get("Retry-After");
|
||||
if (retryAfter != null) {
|
||||
if (/^\d+$/.test(retryAfter)) {
|
||||
const ms = Number.parseInt(retryAfter) * 1000;
|
||||
if (!Number.isFinite(ms)) {
|
||||
throw new Error("Retry-After header integer value is too large");
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
const date = new Date(retryAfter);
|
||||
if (date.toUTCString() !== retryAfter) {
|
||||
throw new Error("Retry-After header value is not a valid HTTP-date or non-negative decimal integer");
|
||||
}
|
||||
return date.getTime() - Date.now();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given (JSON-parsed) response body looks like a Matrix error
|
||||
* response as specified in https://spec.matrix.org/v1.19/client-server-api/#standard-error-response
|
||||
*
|
||||
* @param response - the parsed response body to check
|
||||
* @returns whether the response is a valid {@link MatrixError}
|
||||
*/
|
||||
export function isMatrixErrorResponse(response: unknown): response is MatrixError {
|
||||
return (
|
||||
isRecord(response) &&
|
||||
hasRequiredStringProperty(response, "error") &&
|
||||
hasRequiredStringProperty(response, "errcode")
|
||||
);
|
||||
}
|
||||
|
||||
export class MatrixError extends HTTPError {
|
||||
// The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
|
||||
public readonly errcode?: string;
|
||||
// The Matrix 'error' value.
|
||||
public readonly error?: string;
|
||||
// The raw Matrix error JSON used to construct this object.
|
||||
public data: IErrorJson;
|
||||
|
||||
@@ -109,14 +46,12 @@ export class MatrixError extends HTTPError {
|
||||
* information specific to the standard Matrix error response.
|
||||
* @param errorJson - The Matrix error JSON returned from the homeserver.
|
||||
* @param httpStatus - The numeric HTTP status code given
|
||||
* @param httpHeaders - The HTTP response headers given
|
||||
*/
|
||||
public constructor(
|
||||
errorJson: IErrorJson = {},
|
||||
httpStatus?: number,
|
||||
public readonly httpStatus?: number,
|
||||
public url?: string,
|
||||
public event?: MatrixEvent,
|
||||
httpHeaders?: Headers,
|
||||
) {
|
||||
let message = errorJson.error || "Unknown message";
|
||||
if (httpStatus) {
|
||||
@@ -125,81 +60,11 @@ export class MatrixError extends HTTPError {
|
||||
if (url) {
|
||||
message = `${message} (${url})`;
|
||||
}
|
||||
super(`MatrixError: ${message}`, httpStatus, httpHeaders);
|
||||
super(`MatrixError: ${message}`, httpStatus);
|
||||
this.errcode = errorJson.errcode;
|
||||
this.error = errorJson.error;
|
||||
this.name = errorJson.errcode || "Unknown error code";
|
||||
this.data = errorJson;
|
||||
}
|
||||
|
||||
public isRateLimitError(): boolean {
|
||||
return (
|
||||
this.errcode === "M_LIMIT_EXCEEDED" ||
|
||||
((this.errcode === "M_UNKNOWN" || this.errcode === undefined) && super.isRateLimitError())
|
||||
);
|
||||
}
|
||||
|
||||
public getRetryAfterMs(): number | null {
|
||||
const headerValue = super.getRetryAfterMs();
|
||||
if (headerValue !== null) {
|
||||
return headerValue;
|
||||
}
|
||||
// Note: retry_after_ms is deprecated as of spec version v1.10
|
||||
if (this.errcode === "M_LIMIT_EXCEEDED" && "retry_after_ms" in this.data) {
|
||||
if (!Number.isInteger(this.data.retry_after_ms)) {
|
||||
throw new Error("retry_after_ms is not an integer");
|
||||
}
|
||||
return this.data.retry_after_ms;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns this error expressed as a JSON payload
|
||||
* for use by Widget API error responses.
|
||||
*/
|
||||
public asWidgetApiErrorData(): IWidgetMatrixError {
|
||||
const headers: Record<string, string> = {};
|
||||
if (this.httpHeaders) {
|
||||
for (const [name, value] of this.httpHeaders) {
|
||||
headers[name] = value;
|
||||
}
|
||||
}
|
||||
return {
|
||||
http_status: this.httpStatus ?? 400,
|
||||
http_headers: headers,
|
||||
url: this.url ?? "",
|
||||
response: {
|
||||
errcode: this.errcode ?? "M_UNKNOWN",
|
||||
error: this.data.error ?? "Unknown message",
|
||||
...this.data,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a new {@link MatrixError} from a JSON payload
|
||||
* received from Widget API error responses.
|
||||
*/
|
||||
public static fromWidgetApiErrorData(data: IWidgetMatrixError): MatrixError {
|
||||
return new MatrixError(data.response, data.http_status, data.url, undefined, new Headers(data.http_headers));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The recommended delay in milliseconds to wait before retrying the request.
|
||||
* @param error - The error to check for a retry delay.
|
||||
* @param defaultMs - The delay to use if the error was not due to rate-limiting or if no valid delay is recommended.
|
||||
*/
|
||||
export function safeGetRetryAfterMs(error: unknown, defaultMs: number): number {
|
||||
if (!(error instanceof HTTPError) || !error.isRateLimitError()) {
|
||||
return defaultMs;
|
||||
}
|
||||
try {
|
||||
return error.getRetryAfterMs() ?? defaultMs;
|
||||
} catch {
|
||||
return defaultMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,61 +82,3 @@ export class ConnectionError extends Error {
|
||||
return "ConnectionError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a TokenRefreshError. This indicates that a request failed due to the token being expired,
|
||||
* and attempting to refresh said token also failed but in a way which was not indicative of token invalidation.
|
||||
* Assumed to be a temporary failure.
|
||||
*/
|
||||
export class TokenRefreshError extends Error {
|
||||
public constructor(cause?: Error) {
|
||||
super(cause?.message ?? "");
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return "TokenRefreshError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a TokenRefreshError. This indicates that a request failed due to the token being expired,
|
||||
* and attempting to refresh said token failed in a way indicative of token invalidation.
|
||||
*/
|
||||
export class TokenRefreshLogoutError extends Error {
|
||||
public constructor(cause?: Error) {
|
||||
super(cause?.message ?? "");
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return "TokenRefreshLogoutError";
|
||||
}
|
||||
}
|
||||
|
||||
export const MatrixSafetyErrorCode = new NamespacedValue(null, "ORG.MATRIX.MSC4387_SAFETY");
|
||||
|
||||
/***
|
||||
* This error is thrown when the homeserver refuses to handle an action due to a
|
||||
* safety concern.
|
||||
* @see https://github.com/matrix-org/matrix-spec-proposals/pull/4387
|
||||
*/
|
||||
export class MatrixSafetyError extends MatrixError {
|
||||
/**
|
||||
* The kinds of harms detected by the server.
|
||||
* @see https://github.com/matrix-org/matrix-spec-proposals/pull/4387 for a list of spec defined harms.
|
||||
*/
|
||||
public readonly harms: Set<string>;
|
||||
/**
|
||||
* The date at which a request can be reattempted.
|
||||
*/
|
||||
public readonly expiry?: Date;
|
||||
public constructor(...props: ConstructorParameters<typeof MatrixError>) {
|
||||
super(...props);
|
||||
const body = props[0];
|
||||
|
||||
this.harms = new Set(body && "harms" in body && Array.isArray(body.harms) ? body.harms : []);
|
||||
this.message = `${super.message} (${[...this.harms].join(", ")})`;
|
||||
if (body && "expiry" in body && typeof body.expiry === "number") {
|
||||
this.expiry = new Date(body.expiry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
219
node_modules/matrix-js-sdk/src/http-api/fetch.ts
generated
vendored
219
node_modules/matrix-js-sdk/src/http-api/fetch.ts
generated
vendored
@@ -18,37 +18,35 @@ limitations under the License.
|
||||
* This is an internal module. See {@link MatrixHttpApi} for the public class.
|
||||
*/
|
||||
|
||||
import { checkObjectHasKeys, deepCopy, encodeParams } from "../utils.ts";
|
||||
import { type TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
import { Method } from "./method.ts";
|
||||
import { ConnectionError, MatrixError, TokenRefreshError } from "./errors.ts";
|
||||
import {
|
||||
type BaseRequestOpts,
|
||||
HttpApiEvent,
|
||||
type HttpApiEventHandlerMap,
|
||||
type IHttpOpts,
|
||||
type IRequestOpts,
|
||||
type Body,
|
||||
} from "./interface.ts";
|
||||
import { anySignal, parseErrorResponse, timeoutSignal } from "./utils.ts";
|
||||
import { type QueryDict } from "../utils.ts";
|
||||
import { TokenRefresher, TokenRefreshOutcome } from "./refresh.ts";
|
||||
import { checkObjectHasKeys, encodeParams } from "../utils";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { Method } from "./method";
|
||||
import { ConnectionError, MatrixError } from "./errors";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IRequestOpts, Body } from "./interface";
|
||||
import { anySignal, parseErrorResponse, timeoutSignal } from "./utils";
|
||||
import { QueryDict } from "../utils";
|
||||
import { logger } from "../logger";
|
||||
|
||||
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();
|
||||
private readonly tokenRefresher: TokenRefresher;
|
||||
|
||||
public constructor(
|
||||
private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
|
||||
public readonly opts: O,
|
||||
) {
|
||||
checkObjectHasKeys(opts, ["baseUrl", "prefix"]);
|
||||
if (!opts.onlyData) {
|
||||
throw new Error("Constructing FetchHttpApi without `onlyData=true` is no longer supported.");
|
||||
}
|
||||
opts.onlyData = !!opts.onlyData;
|
||||
opts.useAuthorizationHeader = opts.useAuthorizationHeader ?? true;
|
||||
|
||||
this.tokenRefresher = new TokenRefresher(opts);
|
||||
}
|
||||
|
||||
public abort(): void {
|
||||
@@ -56,11 +54,11 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
this.abortController = new AbortController();
|
||||
}
|
||||
|
||||
public fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof globalThis.fetch> {
|
||||
public fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
|
||||
if (this.opts.fetchFn) {
|
||||
return this.opts.fetchFn(resource, options);
|
||||
}
|
||||
return globalThis.fetch(resource, options);
|
||||
return global.fetch(resource, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,13 +69,13 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
this.opts.idBaseUrl = url;
|
||||
}
|
||||
|
||||
public idServerRequest<T extends object = Record<string, unknown>>(
|
||||
public idServerRequest<T extends {} = Record<string, unknown>>(
|
||||
method: Method,
|
||||
path: string,
|
||||
params: Record<string, string | string[]> | undefined,
|
||||
prefix: string,
|
||||
accessToken?: string,
|
||||
): Promise<T> {
|
||||
): Promise<ResponseType<T, O>> {
|
||||
if (!this.opts.idBaseUrl) {
|
||||
throw new Error("No identity server base URL set");
|
||||
}
|
||||
@@ -114,82 +112,59 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
*
|
||||
* @param body - The HTTP JSON body.
|
||||
*
|
||||
* @param paramOpts - additional options.
|
||||
* When `paramOpts.doNotAttemptTokenRefresh` is true, token refresh will not be attempted
|
||||
* when an expired token is encountered. Used to only attempt token refresh once.
|
||||
* @param opts - additional options. If a number is specified,
|
||||
* this is treated as `opts.localTimeoutMs`.
|
||||
*
|
||||
* @returns The parsed response.
|
||||
* @throws Error if a problem occurred. This includes network problems and Matrix-specific error JSON.
|
||||
* @returns Promise which resolves to
|
||||
* ```
|
||||
* {
|
||||
* data: {Object},
|
||||
* headers: {Object},
|
||||
* code: {Number},
|
||||
* }
|
||||
* ```
|
||||
* If `onlyData` is set, this will resolve to the `data` object only.
|
||||
* @returns 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 = {},
|
||||
queryParams?: QueryDict,
|
||||
body?: Body,
|
||||
paramOpts: IRequestOpts = {},
|
||||
): Promise<T> {
|
||||
return this.doAuthedRequest<T>(1, method, path, queryParams, body, paramOpts);
|
||||
}
|
||||
opts: IRequestOpts = {},
|
||||
): Promise<ResponseType<T, O>> {
|
||||
if (!queryParams) queryParams = {};
|
||||
|
||||
// Wrapper around public method authedRequest to allow for tracking retry attempt counts
|
||||
private async doAuthedRequest<T>(
|
||||
attempt: number,
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams: QueryDict,
|
||||
body?: Body,
|
||||
paramOpts: IRequestOpts = {},
|
||||
): Promise<T> {
|
||||
// avoid mutating paramOpts so they can be used on retry
|
||||
const opts = deepCopy(paramOpts);
|
||||
// we have to manually copy the abortSignal over as it is not a plain object
|
||||
opts.abortSignal = paramOpts.abortSignal;
|
||||
|
||||
// Take a snapshot of the current token state before we start the request so we can reference it if we error
|
||||
const requestSnapshot = await this.tokenRefresher.prepareForRequest();
|
||||
if (requestSnapshot.accessToken) {
|
||||
if (this.opts.accessToken) {
|
||||
if (this.opts.useAuthorizationHeader) {
|
||||
if (!opts.headers) {
|
||||
opts.headers = {};
|
||||
}
|
||||
if (!opts.headers.Authorization) {
|
||||
opts.headers.Authorization = `Bearer ${requestSnapshot.accessToken}`;
|
||||
opts.headers.Authorization = "Bearer " + this.opts.accessToken;
|
||||
}
|
||||
if (queryParams.access_token) {
|
||||
delete queryParams.access_token;
|
||||
}
|
||||
} else if (!queryParams.access_token) {
|
||||
queryParams.access_token = requestSnapshot.accessToken;
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.request<T>(method, path, queryParams, body, opts);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (!(error instanceof MatrixError)) {
|
||||
throw error;
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
if (error.errcode === "M_UNKNOWN_TOKEN") {
|
||||
const outcome = await this.tokenRefresher.handleUnknownToken(requestSnapshot, attempt);
|
||||
if (outcome === TokenRefreshOutcome.Success) {
|
||||
// if we got a new token retry the request
|
||||
return this.doAuthedRequest(attempt + 1, method, path, queryParams, body, paramOpts);
|
||||
}
|
||||
if (outcome === TokenRefreshOutcome.Failure) {
|
||||
throw new TokenRefreshError(error);
|
||||
}
|
||||
|
||||
if (!opts?.inhibitLogoutEmit) {
|
||||
this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, error);
|
||||
}
|
||||
} else if (error.errcode == "M_CONSENT_NOT_GIVEN") {
|
||||
this.eventEmitter.emit(HttpApiEvent.NoConsent, error.message, error.data.consent_uri);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,16 +180,26 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
*
|
||||
* @param opts - additional options
|
||||
*
|
||||
* @returns The parsed response.
|
||||
* @throws Error if a problem occurred. This includes network problems and Matrix-specific error JSON.
|
||||
* @returns Promise which resolves to
|
||||
* ```
|
||||
* {
|
||||
* data: {Object},
|
||||
* headers: {Object},
|
||||
* code: {Number},
|
||||
* }
|
||||
* ```
|
||||
* If `onlyData</code> is set, this will resolve to the <code>data`
|
||||
* object only.
|
||||
* @returns Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public request<T = unknown>(
|
||||
public request<T>(
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: QueryDict,
|
||||
body?: Body,
|
||||
opts?: IRequestOpts,
|
||||
): Promise<T> {
|
||||
): Promise<ResponseType<T, O>> {
|
||||
const fullUri = this.getUrl(path, queryParams, opts?.prefix, opts?.baseUrl);
|
||||
return this.requestOtherUrl<T>(method, fullUri, body, opts);
|
||||
}
|
||||
@@ -228,27 +213,30 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
*
|
||||
* @param opts - additional options
|
||||
*
|
||||
* @returns The parsed response.
|
||||
* @throws Error if a problem occurred. This includes network problems and Matrix-specific error JSON.
|
||||
* @returns Promise which resolves to data unless `onlyData` is specified as false,
|
||||
* where the resolved value will be a fetch Response object.
|
||||
* @returns 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: BaseRequestOpts = {},
|
||||
): Promise<T> {
|
||||
if (opts.json !== undefined && opts.rawResponseBody !== undefined) {
|
||||
throw new Error("Invalid call to `FetchHttpApi` sets both `opts.json` and `opts.rawResponseBody`");
|
||||
}
|
||||
|
||||
opts: Pick<IRequestOpts, "headers" | "json" | "localTimeoutMs" | "keepAlive" | "abortSignal" | "priority"> = {},
|
||||
): Promise<ResponseType<T, O>> {
|
||||
const urlForLogs = this.sanitizeUrlForLogs(url);
|
||||
|
||||
this.opts.logger?.debug(`FetchHttpApi: --> ${method} ${urlForLogs}`);
|
||||
logger.debug(`FetchHttpApi: --> ${method} ${urlForLogs}`);
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
const jsonResponse = !opts.rawResponseBody && opts.json !== false;
|
||||
if (jsonResponse) {
|
||||
if (!headers["Accept"]) {
|
||||
headers["Accept"] = "application/json";
|
||||
}
|
||||
@@ -264,28 +252,15 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
signals.push(opts.abortSignal);
|
||||
}
|
||||
|
||||
// If the body is an object, encode it as JSON and set the `Content-Type` header,
|
||||
// unless that has been explicitly inhibited by setting `opts.json: false`.
|
||||
// We can't use getPrototypeOf here as objects made in other contexts e.g. over postMessage won't have same ref
|
||||
let data: BodyInit;
|
||||
if (opts.json !== false && body?.constructor?.name === Object.name) {
|
||||
if (jsonBody) {
|
||||
data = JSON.stringify(body);
|
||||
if (!headers["Content-Type"]) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
} else {
|
||||
data = body as BodyInit;
|
||||
}
|
||||
|
||||
const { signal, cleanup } = anySignal(signals);
|
||||
|
||||
// Set cache mode based on presence of Authorization header.
|
||||
// Browsers/proxies do not cache responses to requests with Authorization headers.
|
||||
// So specifying "no-cache" is redundant, and actually prevents caching
|
||||
// of preflight requests in CORS scenarios. As such, we only set "no-cache"
|
||||
// when there is no Authorization header.
|
||||
const cacheMode = "Authorization" in headers ? undefined : "no-cache";
|
||||
|
||||
let res: Response;
|
||||
const start = Date.now();
|
||||
try {
|
||||
@@ -298,17 +273,15 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
redirect: "follow",
|
||||
referrer: "",
|
||||
referrerPolicy: "no-referrer",
|
||||
cache: cacheMode,
|
||||
cache: "no-cache",
|
||||
credentials: "omit", // we send credentials via headers
|
||||
keepalive: keepAlive,
|
||||
priority: opts.priority,
|
||||
});
|
||||
|
||||
this.opts.logger?.debug(
|
||||
`FetchHttpApi: <-- ${method} ${urlForLogs} [${Date.now() - start}ms ${res.status}]`,
|
||||
);
|
||||
logger.debug(`FetchHttpApi: <-- ${method} ${urlForLogs} [${Date.now() - start}ms ${res.status}]`);
|
||||
} catch (e) {
|
||||
this.opts.logger?.debug(`FetchHttpApi: <-- ${method} ${urlForLogs} [${Date.now() - start}ms ${e}]`);
|
||||
logger.debug(`FetchHttpApi: <-- ${method} ${urlForLogs} [${Date.now() - start}ms ${e}]`);
|
||||
if ((<Error>e).name === "AbortError") {
|
||||
throw e;
|
||||
}
|
||||
@@ -321,13 +294,10 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
throw parseErrorResponse(res, await res.text());
|
||||
}
|
||||
|
||||
if (opts.rawResponseBody) {
|
||||
return (await res.blob()) as T;
|
||||
} else if (jsonResponse) {
|
||||
return await res.json();
|
||||
} else {
|
||||
return (await res.text()) as T;
|
||||
if (this.opts.onlyData) {
|
||||
return json ? res.json() : res.text();
|
||||
}
|
||||
return res as ResponseType<T, O>;
|
||||
}
|
||||
|
||||
private sanitizeUrlForLogs(url: URL | string): string {
|
||||
@@ -347,7 +317,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
const sanitizedQsUrlPiece = sanitizedQsString ? `?${sanitizedQsString}` : "";
|
||||
|
||||
return asUrl.origin + asUrl.pathname + sanitizedQsUrlPiece;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// defensive coding for malformed url
|
||||
return "??";
|
||||
}
|
||||
@@ -366,12 +336,9 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
? baseUrlWithFallback.slice(0, -1)
|
||||
: baseUrlWithFallback;
|
||||
const url = new URL(baseUrlWithoutTrailingSlash + (prefix ?? this.opts.prefix) + path);
|
||||
// If there are any params, encode and append them to the URL.
|
||||
if (this.opts.extraParams || queryParams) {
|
||||
const mergedParams = { ...this.opts.extraParams, ...queryParams };
|
||||
encodeParams(mergedParams, url.searchParams);
|
||||
if (queryParams) {
|
||||
encodeParams(queryParams, url.searchParams);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
81
node_modules/matrix-js-sdk/src/http-api/index.ts
generated
vendored
81
node_modules/matrix-js-sdk/src/http-api/index.ts
generated
vendored
@@ -14,20 +14,20 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { FetchHttpApi } from "./fetch.ts";
|
||||
import { type FileType, type IHttpOpts, type Upload, type UploadOpts, type UploadResponse } from "./interface.ts";
|
||||
import { MediaPrefix } from "./prefix.ts";
|
||||
import { type QueryDict, removeElement } from "../utils.ts";
|
||||
import * as callbacks from "../realtime-callbacks.ts";
|
||||
import { Method } from "./method.ts";
|
||||
import { ConnectionError } from "./errors.ts";
|
||||
import { parseErrorResponse } from "./utils.ts";
|
||||
import { FetchHttpApi } from "./fetch";
|
||||
import { FileType, IContentUri, IHttpOpts, Upload, UploadOpts, UploadResponse } from "./interface";
|
||||
import { MediaPrefix } from "./prefix";
|
||||
import { defer, QueryDict, removeElement } from "../utils";
|
||||
import * as callbacks from "../realtime-callbacks";
|
||||
import { Method } from "./method";
|
||||
import { ConnectionError } from "./errors";
|
||||
import { parseErrorResponse } from "./utils";
|
||||
|
||||
export * from "./interface.ts";
|
||||
export * from "./prefix.ts";
|
||||
export * from "./errors.ts";
|
||||
export * from "./method.ts";
|
||||
export * from "./utils.ts";
|
||||
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[] = [];
|
||||
@@ -41,16 +41,16 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
*
|
||||
* @param opts - options object
|
||||
*
|
||||
* @returns Promise which resolves to response object, or rejects with an error (usually a MatrixError).
|
||||
* @throws May throw a `MatrixSafetyError` if content is deemed unsafe.
|
||||
* @see MatrixSafetyError
|
||||
* @returns Promise which 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 contentType = opts.type ?? (file as File).type ?? "application/octet-stream";
|
||||
const fileName = opts.name ?? (file as File).name;
|
||||
|
||||
const upload = {
|
||||
@@ -58,14 +58,14 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
total: 0,
|
||||
abortController,
|
||||
} as Upload;
|
||||
const uploadResolvers = Promise.withResolvers<UploadResponse>();
|
||||
const deferred = defer<UploadResponse>();
|
||||
|
||||
if (globalThis.XMLHttpRequest) {
|
||||
const xhr = new globalThis.XMLHttpRequest();
|
||||
if (global.XMLHttpRequest) {
|
||||
const xhr = new global.XMLHttpRequest();
|
||||
|
||||
const timeoutFn = function (): void {
|
||||
xhr.abort();
|
||||
uploadResolvers.reject(new Error("Timeout"));
|
||||
deferred.reject(new Error("Timeout"));
|
||||
};
|
||||
|
||||
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
|
||||
@@ -73,7 +73,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
|
||||
xhr.onreadystatechange = function (): void {
|
||||
switch (xhr.readyState) {
|
||||
case globalThis.XMLHttpRequest.DONE:
|
||||
case global.XMLHttpRequest.DONE:
|
||||
callbacks.clearTimeout(timeoutTimer);
|
||||
try {
|
||||
if (xhr.status === 0) {
|
||||
@@ -84,16 +84,16 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
}
|
||||
|
||||
if (xhr.status >= 400) {
|
||||
uploadResolvers.reject(parseErrorResponse(xhr, xhr.responseText));
|
||||
deferred.reject(parseErrorResponse(xhr, xhr.responseText));
|
||||
} else {
|
||||
uploadResolvers.resolve(JSON.parse(xhr.responseText));
|
||||
deferred.resolve(JSON.parse(xhr.responseText));
|
||||
}
|
||||
} catch (err) {
|
||||
if ((<Error>err).name === "AbortError") {
|
||||
uploadResolvers.reject(err);
|
||||
deferred.reject(err);
|
||||
return;
|
||||
}
|
||||
uploadResolvers.reject(new ConnectionError("request failed", <Error>err));
|
||||
deferred.reject(new ConnectionError("request failed", <Error>err));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -110,7 +110,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
});
|
||||
};
|
||||
|
||||
const url = this.getUrl("/upload", undefined, MediaPrefix.V3);
|
||||
const url = this.getUrl("/upload", undefined, MediaPrefix.R0);
|
||||
|
||||
if (includeFilename && fileName) {
|
||||
url.searchParams.set("filename", encodeURIComponent(fileName));
|
||||
@@ -139,19 +139,23 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
const headers: Record<string, string> = { "Content-Type": contentType };
|
||||
|
||||
this.authedRequest<UploadResponse>(Method.Post, "/upload", queryParams, file, {
|
||||
prefix: MediaPrefix.V3,
|
||||
prefix: MediaPrefix.R0,
|
||||
headers,
|
||||
abortSignal: abortController.signal,
|
||||
}).then(uploadResolvers.resolve, uploadResolvers.reject);
|
||||
})
|
||||
.then((response) => {
|
||||
return this.opts.onlyData ? <UploadResponse>response : response.json();
|
||||
})
|
||||
.then(deferred.resolve, deferred.reject);
|
||||
}
|
||||
|
||||
// remove the upload from the list on completion
|
||||
upload.promise = uploadResolvers.promise.finally(() => {
|
||||
upload.promise = deferred.promise.finally(() => {
|
||||
removeElement(this.uploads, (elem) => elem === upload);
|
||||
});
|
||||
abortController.signal.addEventListener("abort", () => {
|
||||
removeElement(this.uploads, (elem) => elem === upload);
|
||||
uploadResolvers.reject(new DOMException("Aborted", "AbortError"));
|
||||
deferred.reject(new DOMException("Aborted", "AbortError"));
|
||||
});
|
||||
this.uploads.push(upload);
|
||||
return upload.promise;
|
||||
@@ -169,4 +173,19 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
public getCurrentUploads(): Upload[] {
|
||||
return this.uploads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content repository url with query parameters.
|
||||
* @returns 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!,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
118
node_modules/matrix-js-sdk/src/http-api/interface.ts
generated
vendored
118
node_modules/matrix-js-sdk/src/http-api/interface.ts
generated
vendored
@@ -14,75 +14,36 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MatrixError } from "./errors.ts";
|
||||
import { type Logger } from "../logger.ts";
|
||||
import { type QueryDict } from "../utils.ts";
|
||||
import { MatrixError } from "./errors";
|
||||
|
||||
export type Body = Record<string, any> | BodyInit;
|
||||
|
||||
/**
|
||||
* Unencrypted access and (optional) refresh token
|
||||
*/
|
||||
export type AccessTokens = {
|
||||
/**
|
||||
* The new access token to use for authenticated requests
|
||||
*/
|
||||
accessToken: string;
|
||||
/**
|
||||
* The new refresh token to use for refreshing tokens, optional
|
||||
*/
|
||||
refreshToken?: string;
|
||||
/**
|
||||
* Approximate date when the access token will expire, optional
|
||||
*/
|
||||
expiry?: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that performs token refresh using the given refreshToken.
|
||||
* Returns a promise that resolves to the refreshed access and (optional) refresh tokens.
|
||||
*
|
||||
* Can be passed to HttpApi instance as {@link IHttpOpts.tokenRefreshFunction} during client creation {@link ICreateClientOpts}
|
||||
*/
|
||||
export type TokenRefreshFunction = (refreshToken: string) => Promise<AccessTokens>;
|
||||
|
||||
/** Options object for `FetchHttpApi` and {@link MatrixHttpApi}. */
|
||||
export interface IHttpOpts {
|
||||
fetchFn?: typeof globalThis.fetch;
|
||||
fetchFn?: typeof global.fetch;
|
||||
|
||||
baseUrl: string;
|
||||
idBaseUrl?: string;
|
||||
prefix: string;
|
||||
extraParams?: QueryDict;
|
||||
extraParams?: Record<string, string>;
|
||||
|
||||
accessToken?: string;
|
||||
/**
|
||||
* Used in conjunction with tokenRefreshFunction to attempt token refresh
|
||||
*/
|
||||
refreshToken?: string;
|
||||
/**
|
||||
* Function to attempt token refresh when a possibly expired token is encountered
|
||||
* Optional, only called when a refreshToken is present
|
||||
*/
|
||||
tokenRefreshFunction?: TokenRefreshFunction;
|
||||
|
||||
/**
|
||||
* Whether to use the HTTP Authorization header over the `access_token` query parameter
|
||||
* @deprecated as of v1.11 in https://spec.matrix.org/v1.17/client-server-api/#using-access-tokens
|
||||
*/
|
||||
useAuthorizationHeader?: boolean; // defaults to true
|
||||
|
||||
/** For historical reasons, must be set to `true`. Will eventually be removed. */
|
||||
onlyData?: boolean;
|
||||
|
||||
localTimeoutMs?: number;
|
||||
|
||||
/** Optional logger instance. If provided, requests and responses will be logged. */
|
||||
logger?: Logger;
|
||||
}
|
||||
|
||||
/** Options object for `FetchHttpApi.requestOtherUrl`. */
|
||||
export interface BaseRequestOpts extends Pick<RequestInit, "priority"> {
|
||||
export interface IRequestOpts extends Pick<RequestInit, "priority"> {
|
||||
/**
|
||||
* The alternative base url to use.
|
||||
* If not specified, uses this.opts.baseUrl
|
||||
*/
|
||||
baseUrl?: string;
|
||||
/**
|
||||
* The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*/
|
||||
prefix?: string;
|
||||
/**
|
||||
* map of additional request headers
|
||||
*/
|
||||
@@ -94,46 +55,7 @@ export interface BaseRequestOpts extends Pick<RequestInit, "priority"> {
|
||||
*/
|
||||
localTimeoutMs?: number;
|
||||
keepAlive?: boolean; // defaults to false
|
||||
|
||||
/**
|
||||
* By default, we will:
|
||||
*
|
||||
* * If the `body` is an object, JSON-encode it and set `Content-Type: application/json` in the
|
||||
* request headers (unless overridden by {@link headers}).
|
||||
*
|
||||
* * Set `Accept: application/json` in the request headers (again, unless overridden by {@link headers}).
|
||||
*
|
||||
* * Parse the response as JSON and return the parsed response.
|
||||
*
|
||||
* Setting this to `false` inhibits all three behaviors, and the response is instead parsed as a UTF-8 string. It
|
||||
* defaults to `true`, unless {@link rawResponseBody} is set.
|
||||
*
|
||||
* @deprecated Instead of setting this to `false`, set {@link rawResponseBody} to `true`.
|
||||
*/
|
||||
json?: boolean;
|
||||
|
||||
/**
|
||||
* Setting this to `true` does two things:
|
||||
*
|
||||
* * Inhibits the automatic addition of `Accept: application/json` in the request headers.
|
||||
*
|
||||
* * Causes the raw response to be returned as a {@link https://developer.mozilla.org/en-US/docs/Web/API/Blob|Blob}
|
||||
* instead of parsing it as JSON.
|
||||
*/
|
||||
rawResponseBody?: boolean;
|
||||
}
|
||||
|
||||
export interface IRequestOpts extends BaseRequestOpts {
|
||||
/**
|
||||
* The alternative base url to use.
|
||||
* If not specified, uses this.opts.baseUrl
|
||||
*/
|
||||
baseUrl?: string;
|
||||
/**
|
||||
* The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*/
|
||||
prefix?: string;
|
||||
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,
|
||||
@@ -141,6 +63,15 @@ export interface IRequestOpts extends BaseRequestOpts {
|
||||
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",
|
||||
@@ -211,6 +142,7 @@ export interface Upload {
|
||||
}
|
||||
|
||||
export interface UploadResponse {
|
||||
// eslint-disable-next-line camelcase
|
||||
content_uri: string;
|
||||
}
|
||||
|
||||
|
||||
3
node_modules/matrix-js-sdk/src/http-api/method.ts
generated
vendored
3
node_modules/matrix-js-sdk/src/http-api/method.ts
generated
vendored
@@ -19,7 +19,4 @@ export enum Method {
|
||||
Put = "PUT",
|
||||
Post = "POST",
|
||||
Delete = "DELETE",
|
||||
Options = "OPTIONS",
|
||||
Head = "HEAD",
|
||||
Patch = "PATCH",
|
||||
}
|
||||
|
||||
14
node_modules/matrix-js-sdk/src/http-api/prefix.ts
generated
vendored
14
node_modules/matrix-js-sdk/src/http-api/prefix.ts
generated
vendored
@@ -16,7 +16,11 @@ limitations under the License.
|
||||
|
||||
export enum ClientPrefix {
|
||||
/**
|
||||
* A constant representing the URI path for Client-Server API endpoints versioned at v1.
|
||||
* 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",
|
||||
/**
|
||||
@@ -38,11 +42,7 @@ export enum IdentityPrefix {
|
||||
|
||||
export enum MediaPrefix {
|
||||
/**
|
||||
* A constant representing the URI path for Client-Server API Media endpoints versioned at v1.
|
||||
* URI path for the media repo API
|
||||
*/
|
||||
V1 = "/_matrix/media/v1",
|
||||
/**
|
||||
* A constant representing the URI path for Client-Server API Media endpoints versioned at v3.
|
||||
*/
|
||||
V3 = "/_matrix/media/v3",
|
||||
R0 = "/_matrix/media/r0",
|
||||
}
|
||||
|
||||
166
node_modules/matrix-js-sdk/src/http-api/refresh.ts
generated
vendored
166
node_modules/matrix-js-sdk/src/http-api/refresh.ts
generated
vendored
@@ -1,166 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 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, TokenRefreshLogoutError } from "./errors.ts";
|
||||
import { type IHttpOpts } from "./interface.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* This is an internal module. See {@link MatrixHttpApi} for the public class.
|
||||
*/
|
||||
|
||||
export const enum TokenRefreshOutcome {
|
||||
Success = "success",
|
||||
Failure = "failure",
|
||||
Logout = "logout",
|
||||
}
|
||||
|
||||
interface Snapshot {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
expiry?: Date;
|
||||
}
|
||||
|
||||
// If the token expires in less than this time amount of time, we will eagerly refresh it before making the intended request.
|
||||
const REFRESH_IF_TOKEN_EXPIRES_WITHIN_MS = 500;
|
||||
// If we get an unknown token error and the token expires in less than this time amount of time, we will refresh it before making the intended request.
|
||||
// Otherwise, we will error as the token should not have expired yet and we need to avoid retrying indefinitely.
|
||||
const REFRESH_ON_ERROR_IF_TOKEN_EXPIRES_WITHIN_MS = 60 * 1000;
|
||||
|
||||
type Opts = Pick<IHttpOpts, "tokenRefreshFunction" | "logger" | "refreshToken" | "accessToken">;
|
||||
|
||||
/**
|
||||
* This class is responsible for managing the access token and refresh token for authenticated requests.
|
||||
* It will automatically refresh the access token when it is about to expire, and will handle unknown token errors.
|
||||
*/
|
||||
export class TokenRefresher {
|
||||
public constructor(private readonly opts: Opts) {}
|
||||
|
||||
/**
|
||||
* Promise used to block authenticated requests during a token refresh to avoid repeated expected errors.
|
||||
* @private
|
||||
*/
|
||||
private tokenRefreshPromise?: Promise<TokenRefreshOutcome>;
|
||||
|
||||
private latestTokenRefreshExpiry?: Date;
|
||||
|
||||
/**
|
||||
* This function is called before every request to ensure that the access token is valid.
|
||||
* @returns a snapshot containing the access token and other properties which must be passed to the handleUnknownToken
|
||||
* handler if an M_UNKNOWN_TOKEN error is encountered.
|
||||
*/
|
||||
public async prepareForRequest(): Promise<Snapshot> {
|
||||
// Ensure our token is refreshed before we build the headers/params
|
||||
await this.refreshIfNeeded();
|
||||
|
||||
return {
|
||||
accessToken: this.opts.accessToken!,
|
||||
refreshToken: this.opts.refreshToken,
|
||||
expiry: this.latestTokenRefreshExpiry,
|
||||
};
|
||||
}
|
||||
|
||||
private async refreshIfNeeded(): Promise<unknown> {
|
||||
if (this.tokenRefreshPromise) {
|
||||
return this.tokenRefreshPromise;
|
||||
}
|
||||
// If we don't know the token expiry, we can't eagerly refresh
|
||||
if (!this.latestTokenRefreshExpiry) return;
|
||||
|
||||
const expiresIn = this.latestTokenRefreshExpiry.getTime() - Date.now();
|
||||
if (expiresIn <= REFRESH_IF_TOKEN_EXPIRES_WITHIN_MS) {
|
||||
await this._handleUnknownToken();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when an M_UNKNOWN_TOKEN error is encountered.
|
||||
* It will attempt to refresh the access token if it is unknown, and will return a TokenRefreshOutcome.
|
||||
* @param snapshot - the snapshot returned by prepareForRequest
|
||||
* @param attempt - the number of attempts made for this request so far
|
||||
* @returns a TokenRefreshOutcome indicating the result of the refresh attempt
|
||||
*/
|
||||
public async handleUnknownToken(snapshot: Snapshot, attempt: number): Promise<TokenRefreshOutcome> {
|
||||
return this._handleUnknownToken(snapshot, attempt);
|
||||
}
|
||||
|
||||
private async _handleUnknownToken(): Promise<TokenRefreshOutcome>;
|
||||
private async _handleUnknownToken(snapshot: Snapshot, attempt: number): Promise<TokenRefreshOutcome>;
|
||||
private async _handleUnknownToken(snapshot?: Snapshot, attempt?: number): Promise<TokenRefreshOutcome> {
|
||||
if (snapshot?.expiry) {
|
||||
// If our token is unknown, but it should not have expired yet, then we should not refresh
|
||||
const expiresIn = snapshot.expiry.getTime() - Date.now();
|
||||
// If it still has plenty of time left on the clock, we assume something else must be wrong and
|
||||
// do not refresh. Otherwise if it's expired, or will soon, we try refreshing.
|
||||
if (expiresIn >= REFRESH_ON_ERROR_IF_TOKEN_EXPIRES_WITHIN_MS) {
|
||||
return TokenRefreshOutcome.Logout;
|
||||
}
|
||||
}
|
||||
|
||||
if (!snapshot || snapshot?.accessToken === this.opts.accessToken) {
|
||||
// If we have a snapshot, but the access token is the same as the current one then a refresh
|
||||
// did not happen behind us but one may be ongoing anyway
|
||||
this.tokenRefreshPromise ??= this.doTokenRefresh(attempt);
|
||||
|
||||
try {
|
||||
return await this.tokenRefreshPromise;
|
||||
} finally {
|
||||
this.tokenRefreshPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// We may end up here if the token was refreshed in the background due to another request
|
||||
return TokenRefreshOutcome.Success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to refresh access tokens.
|
||||
* On success, sets new access and refresh tokens in opts.
|
||||
* @returns Promise that resolves to a boolean - true when token was refreshed successfully
|
||||
*/
|
||||
private async doTokenRefresh(attempt?: number): Promise<TokenRefreshOutcome> {
|
||||
if (!this.opts.refreshToken || !this.opts.tokenRefreshFunction) {
|
||||
this.opts.logger?.error("Unable to refresh token - no refresh token or refresh function");
|
||||
return TokenRefreshOutcome.Logout;
|
||||
}
|
||||
|
||||
if (attempt && attempt > 1) {
|
||||
// Exponential backoff to ensure we don't trash the server, up to 2^5 seconds
|
||||
await sleep(1000 * Math.min(32, 2 ** attempt));
|
||||
}
|
||||
|
||||
try {
|
||||
this.opts.logger?.debug("Attempting to refresh token");
|
||||
const { accessToken, refreshToken, expiry } = await this.opts.tokenRefreshFunction(this.opts.refreshToken);
|
||||
this.opts.accessToken = accessToken;
|
||||
this.opts.refreshToken = refreshToken;
|
||||
this.latestTokenRefreshExpiry = expiry;
|
||||
this.opts.logger?.debug("... token refresh complete, new token expiry:", expiry);
|
||||
|
||||
// successfully got new tokens
|
||||
return TokenRefreshOutcome.Success;
|
||||
} catch (error) {
|
||||
// If we get a TokenError or MatrixError, we should log out, otherwise assume transient
|
||||
if (error instanceof TokenRefreshLogoutError || error instanceof MatrixError) {
|
||||
this.opts.logger?.error("Failed to refresh token", error);
|
||||
return TokenRefreshOutcome.Logout;
|
||||
}
|
||||
|
||||
this.opts.logger?.warn("Failed to refresh token", error);
|
||||
return TokenRefreshOutcome.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
115
node_modules/matrix-js-sdk/src/http-api/utils.ts
generated
vendored
115
node_modules/matrix-js-sdk/src/http-api/utils.ts
generated
vendored
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 - 2024 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
@@ -14,18 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { parse as parseContentType, type ContentType } from "content-type";
|
||||
import { parse as parseContentType, ParsedMediaType } from "content-type";
|
||||
|
||||
import { logger } from "../logger.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
import {
|
||||
ConnectionError,
|
||||
HTTPError,
|
||||
MatrixError,
|
||||
MatrixSafetyError,
|
||||
MatrixSafetyErrorCode,
|
||||
safeGetRetryAfterMs,
|
||||
} from "./errors.ts";
|
||||
import { logger } from "../logger";
|
||||
import { sleep } from "../utils";
|
||||
import { ConnectionError, HTTPError, MatrixError } from "./errors";
|
||||
|
||||
// Ponyfill for https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
|
||||
export function timeoutSignal(ms: number): AbortSignal {
|
||||
@@ -39,7 +32,7 @@ export function timeoutSignal(ms: number): AbortSignal {
|
||||
|
||||
export function anySignal(signals: AbortSignal[]): {
|
||||
signal: AbortSignal;
|
||||
cleanup(this: void): void;
|
||||
cleanup(): void;
|
||||
} {
|
||||
const controller = new AbortController();
|
||||
|
||||
@@ -79,48 +72,24 @@ export function anySignal(signals: AbortSignal[]): {
|
||||
* @returns
|
||||
*/
|
||||
export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error {
|
||||
const httpHeaders = isXhr(response)
|
||||
? new Headers(
|
||||
response
|
||||
.getAllResponseHeaders()
|
||||
.trim()
|
||||
.split(/[\r\n]+/)
|
||||
.map((header): [string, string] => {
|
||||
const colonIdx = header.indexOf(":");
|
||||
return [header.substring(0, colonIdx), header.substring(colonIdx + 1)];
|
||||
}),
|
||||
)
|
||||
: response.headers;
|
||||
|
||||
let contentType: ContentType | null;
|
||||
let contentType: ParsedMediaType | null;
|
||||
try {
|
||||
contentType = getResponseContentType(httpHeaders);
|
||||
contentType = getResponseContentType(response);
|
||||
} catch (e) {
|
||||
return <Error>e;
|
||||
}
|
||||
|
||||
if (contentType?.type === "application/json" && body) {
|
||||
const errorBody = JSON.parse(body);
|
||||
if (errorBody.errcode && MatrixSafetyErrorCode.matches(errorBody.errcode)) {
|
||||
return new MatrixSafetyError(
|
||||
errorBody,
|
||||
response.status,
|
||||
isXhr(response) ? response.responseURL : response.url,
|
||||
undefined,
|
||||
httpHeaders,
|
||||
);
|
||||
}
|
||||
return new MatrixError(
|
||||
errorBody,
|
||||
JSON.parse(body),
|
||||
response.status,
|
||||
isXhr(response) ? response.responseURL : response.url,
|
||||
undefined,
|
||||
httpHeaders,
|
||||
);
|
||||
}
|
||||
if (contentType?.type === "text/plain") {
|
||||
return new HTTPError(`Server returned ${response.status} error: ${body}`, response.status, httpHeaders);
|
||||
return new HTTPError(`Server returned ${response.status} error: ${body}`, response.status);
|
||||
}
|
||||
return new HTTPError(`Server returned ${response.status} error`, response.status, httpHeaders);
|
||||
return new HTTPError(`Server returned ${response.status} error`, response.status);
|
||||
}
|
||||
|
||||
function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest {
|
||||
@@ -128,7 +97,7 @@ function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* extract the Content-Type header from response headers, and
|
||||
* 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.
|
||||
@@ -136,10 +105,21 @@ function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest
|
||||
* @param response - response object
|
||||
* @returns parsed content-type header, or null if not found
|
||||
*/
|
||||
function getResponseContentType(headers: Headers): ContentType | null {
|
||||
const contentType = headers.get("Content-Type");
|
||||
if (contentType === null) return null;
|
||||
return parseContentType(contentType);
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,42 +151,3 @@ export async function retryNetworkOperation<T>(maxAttempts: number, callback: ()
|
||||
}
|
||||
throw lastConnectionError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the backoff time for a request retry attempt.
|
||||
* This produces wait times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
|
||||
* failure was due to a rate limited request, the time specified in the error is returned.
|
||||
*
|
||||
* Returns -1 if the error is not retryable, or if we reach the maximum number of attempts.
|
||||
*
|
||||
* @param err - The error thrown by the http call
|
||||
* @param attempts - The number of attempts made so far, including the one that just failed.
|
||||
* @param retryConnectionError - Whether to retry on {@link ConnectionError} (CORS, connection is down, etc.)
|
||||
*/
|
||||
export function calculateRetryBackoff(err: any, attempts: number, retryConnectionError: boolean): number {
|
||||
if (attempts > 4) {
|
||||
return -1; // give up
|
||||
}
|
||||
|
||||
if (err instanceof ConnectionError && !retryConnectionError) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.httpStatus && Math.floor(err.httpStatus / 100) === 4 && err.httpStatus !== 429) {
|
||||
// client error; no amount of retrying will save you now (except for rate limiting which is handled below)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.name === "AbortError") {
|
||||
// this is a client timeout, that is already very high 60s/80s
|
||||
// we don't want to retry, as it could do it for very long
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If we are trying to send an event (or similar) that is too large in any way, then retrying won't help
|
||||
if (err.name === "M_TOO_LARGE") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return safeGetRetryAfterMs(err, 1000 * Math.pow(2, attempts));
|
||||
}
|
||||
|
||||
8
node_modules/matrix-js-sdk/src/index.ts
generated
vendored
8
node_modules/matrix-js-sdk/src/index.ts
generated
vendored
@@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as matrixcs from "./matrix.ts";
|
||||
import * as matrixcs from "./matrix";
|
||||
|
||||
if (globalThis.__js_sdk_entrypoint) {
|
||||
if (global.__js_sdk_entrypoint) {
|
||||
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
|
||||
}
|
||||
globalThis.__js_sdk_entrypoint = true;
|
||||
global.__js_sdk_entrypoint = true;
|
||||
|
||||
export * from "./matrix.ts";
|
||||
export * from "./matrix";
|
||||
export default matrixcs;
|
||||
|
||||
2
node_modules/matrix-js-sdk/src/indexeddb-worker.ts
generated
vendored
2
node_modules/matrix-js-sdk/src/indexeddb-worker.ts
generated
vendored
@@ -21,4 +21,4 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
/** The {@link IndexedDBStoreWorker} class. */
|
||||
export { IndexedDBStoreWorker } from "./store/indexeddb-store-worker.ts";
|
||||
export { IndexedDBStoreWorker } from "./store/indexeddb-store-worker";
|
||||
|
||||
90
node_modules/matrix-js-sdk/src/interactive-auth.ts
generated
vendored
90
node_modules/matrix-js-sdk/src/interactive-auth.ts
generated
vendored
@@ -16,10 +16,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "./logger.ts";
|
||||
import { type MatrixClient } from "./client.ts";
|
||||
import { MatrixError } from "./http-api/index.ts";
|
||||
import { type UserIdentifier } from "./@types/auth.ts";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixClient } from "./client";
|
||||
import { defer, IDeferred } from "./utils";
|
||||
import { MatrixError } from "./http-api";
|
||||
import { UIAResponse } from "./@types/uia";
|
||||
import { UserIdentifier } from "./@types/auth";
|
||||
|
||||
const EMAIL_STAGE_TYPE = "m.login.email.identity";
|
||||
const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
@@ -85,11 +87,6 @@ export enum AuthType {
|
||||
// use the stable "m.login.registration_token" type.
|
||||
// The authentication flow is the same in both cases.
|
||||
UnstableRegistrationToken = "org.matrix.msc3231.login.registration_token",
|
||||
/**
|
||||
* m.oauth stage introduced by MSC4312:
|
||||
* https://spec.matrix.org/v1.17/client-server-api/#oauth-authentication
|
||||
*/
|
||||
OAuth = "m.oauth",
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,6 +121,10 @@ interface ThreepidCreds {
|
||||
type EmailIdentityDict = {
|
||||
type: AuthType.Email;
|
||||
threepid_creds: ThreepidCreds;
|
||||
/**
|
||||
* @deprecated in favour of `threepid_creds` - kept for backwards compatibility
|
||||
*/
|
||||
threepidCreds?: ThreepidCreds;
|
||||
session: string;
|
||||
};
|
||||
|
||||
@@ -137,17 +138,19 @@ export type AuthDict =
|
||||
| RecaptchaDict
|
||||
| EmailIdentityDict
|
||||
| { type: Exclude<string, AuthType>; [key: string]: any }
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
| {};
|
||||
|
||||
/**
|
||||
* Backwards compatible export
|
||||
* @deprecated in favour of AuthDict
|
||||
*/
|
||||
export type IAuthDict = AuthDict;
|
||||
|
||||
export class NoAuthFlowFoundError extends Error {
|
||||
public name = "NoAuthFlowFoundError";
|
||||
|
||||
public constructor(
|
||||
m: string,
|
||||
public readonly required_stages: string[],
|
||||
public readonly flows: UIAFlow[],
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||
public constructor(m: string, public readonly required_stages: string[], public readonly flows: UIAFlow[]) {
|
||||
super(m);
|
||||
}
|
||||
}
|
||||
@@ -161,7 +164,7 @@ export class NoAuthFlowFoundError extends Error {
|
||||
*
|
||||
* The generic parameter `T` is the type of the response of the endpoint, once it is eventually successful.
|
||||
*/
|
||||
export type UIAuthCallback<T> = (makeRequest: (authData: AuthDict | null) => Promise<T>) => Promise<T>;
|
||||
export type UIAuthCallback<T> = (makeRequest: (authData: IAuthDict | null) => Promise<UIAResponse<T>>) => Promise<T>;
|
||||
|
||||
interface IOpts<T> {
|
||||
/**
|
||||
@@ -204,7 +207,7 @@ interface IOpts<T> {
|
||||
* The busyChanged callback should be used instead of the background flag.
|
||||
* Should return a promise which resolves to the successful response or rejects with a MatrixError.
|
||||
*/
|
||||
doRequest(this: void, auth: AuthDict | null, background: boolean): Promise<T>;
|
||||
doRequest(auth: AuthDict | null, background: boolean): Promise<T>;
|
||||
/**
|
||||
* Called when the status of the UI auth changes,
|
||||
* ie. when the state of an auth stage changes of when the auth flow moves to a new stage.
|
||||
@@ -218,7 +221,7 @@ interface IOpts<T> {
|
||||
* m.login.email.identity:
|
||||
* * emailSid: string, the sid of the active email auth session
|
||||
*/
|
||||
stateUpdated(this: void, nextStage: AuthType | string, status: IStageStatus): void;
|
||||
stateUpdated(nextStage: AuthType | string, status: IStageStatus): void;
|
||||
|
||||
/**
|
||||
* A function that takes the email address (string), clientSecret (string), attempt number (int) and
|
||||
@@ -226,20 +229,14 @@ interface IOpts<T> {
|
||||
* function.
|
||||
* If the resulting promise rejects, the rejection will propagate through to the attemptAuth promise.
|
||||
*/
|
||||
requestEmailToken(
|
||||
this: void,
|
||||
email: string,
|
||||
secret: string,
|
||||
attempt: number,
|
||||
session: string,
|
||||
): Promise<{ sid: string }>;
|
||||
requestEmailToken(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>;
|
||||
/**
|
||||
* Called whenever the interactive auth logic becomes busy submitting information provided by the user or finishes.
|
||||
* After this has been called with true the UI should indicate that a request is in progress
|
||||
* until it is called again with false.
|
||||
*/
|
||||
busyChanged?(this: void, busy: boolean): void;
|
||||
startAuthStage?(this: void, nextStage: string): Promise<void>; // LEGACY
|
||||
busyChanged?(busy: boolean): void;
|
||||
startAuthStage?(nextStage: string): Promise<void>; // LEGACY
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,7 +268,7 @@ export class InteractiveAuth<T> {
|
||||
private data: IAuthData & MatrixError["data"];
|
||||
private emailSid?: string;
|
||||
private requestingEmailToken = false;
|
||||
private attemptAuthDeferred: PromiseWithResolvers<T> | null = null;
|
||||
private attemptAuthDeferred: IDeferred<T> | null = null;
|
||||
private chosenFlow: UIAFlow | null = null;
|
||||
private currentStage: string | null = null;
|
||||
|
||||
@@ -307,7 +304,7 @@ export class InteractiveAuth<T> {
|
||||
public async attemptAuth(): Promise<T> {
|
||||
// This promise will be quite long-lived and will resolve when the
|
||||
// request is authenticated and completes successfully.
|
||||
this.attemptAuthDeferred = Promise.withResolvers();
|
||||
this.attemptAuthDeferred = defer();
|
||||
// pluck the promise out now, as doRequest may clear before we return
|
||||
const promise = this.attemptAuthDeferred.promise;
|
||||
|
||||
@@ -339,7 +336,7 @@ export class InteractiveAuth<T> {
|
||||
// another just to check what the status is
|
||||
if (this.submitPromise) return;
|
||||
|
||||
let authDict: AuthDict = {};
|
||||
let authDict: IAuthDict = {};
|
||||
if (this.currentStage == EMAIL_STAGE_TYPE) {
|
||||
// The email can be validated out-of-band, but we need to provide the
|
||||
// creds so the HS can go & check it.
|
||||
@@ -348,13 +345,17 @@ export class InteractiveAuth<T> {
|
||||
sid: this.emailSid,
|
||||
client_secret: this.clientSecret,
|
||||
};
|
||||
const isUrl = this.matrixClient.getIdentityServerUrl();
|
||||
if (isUrl) {
|
||||
creds.id_server = new URL(isUrl).host;
|
||||
if (await this.matrixClient.doesServerRequireIdServerParam()) {
|
||||
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
|
||||
// See https://github.com/matrix-org/synapse/issues/5665
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
threepid_creds: creds,
|
||||
threepidCreds: creds,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -407,7 +408,7 @@ export class InteractiveAuth<T> {
|
||||
* in the attemptAuth promise being rejected. This can be set to true
|
||||
* for requests that just poll to see if auth has been completed elsewhere.
|
||||
*/
|
||||
public async submitAuthDict(authData: AuthDict, background = false): Promise<void> {
|
||||
public async submitAuthDict(authData: IAuthDict, background = false): Promise<void> {
|
||||
if (!this.attemptAuthDeferred) {
|
||||
throw new Error("submitAuthDict() called before attemptAuth()");
|
||||
}
|
||||
@@ -419,24 +420,21 @@ export class InteractiveAuth<T> {
|
||||
// if we're currently trying a request, wait for it to finish
|
||||
// as otherwise we can get multiple 200 responses which can mean
|
||||
// things like multiple logins for register requests.
|
||||
// (but discard any exceptions as we only care when its done,
|
||||
// not whether it worked or not)
|
||||
while (this.submitPromise) {
|
||||
try {
|
||||
await this.submitPromise;
|
||||
} catch {
|
||||
// discard any exceptions as we only care when its done,
|
||||
// not whether it worked or not
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// use the sessionid from the last request, if one is present.
|
||||
let auth: AuthDict;
|
||||
let auth: IAuthDict;
|
||||
if ((this.data as IAuthData)?.session) {
|
||||
auth = Object.assign(
|
||||
{
|
||||
session: (this.data as IAuthData).session,
|
||||
},
|
||||
authData,
|
||||
);
|
||||
auth = {
|
||||
session: (this.data as IAuthData).session,
|
||||
};
|
||||
Object.assign(auth, authData);
|
||||
} else {
|
||||
auth = authData;
|
||||
}
|
||||
@@ -515,7 +513,7 @@ export class InteractiveAuth<T> {
|
||||
* This can be set to true for requests that just poll to see if auth has
|
||||
* been completed elsewhere.
|
||||
*/
|
||||
private async doRequest(auth: AuthDict | null, background = false): Promise<void> {
|
||||
private async doRequest(auth: IAuthDict | null, background = false): Promise<void> {
|
||||
try {
|
||||
const result = await this.requestCallback(auth, background);
|
||||
this.attemptAuthDeferred!.resolve(result);
|
||||
|
||||
239
node_modules/matrix-js-sdk/src/logger.ts
generated
vendored
239
node_modules/matrix-js-sdk/src/logger.ts
generated
vendored
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 André Jaenisch
|
||||
Copyright 2019-2025 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 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.
|
||||
@@ -15,73 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import loglevel from "loglevel";
|
||||
|
||||
/** Backwards-compatibility hack to expose `log` to applications that might still be relying on it. */
|
||||
interface LoggerWithLogMethod extends Logger {
|
||||
/**
|
||||
* Output debug message to the logger.
|
||||
*
|
||||
* @param msg - Data to log.
|
||||
*
|
||||
* @deprecated prefer {@link Logger.debug}.
|
||||
*/
|
||||
log(...msg: any[]): void;
|
||||
}
|
||||
|
||||
/** Logger interface used within the js-sdk codebase */
|
||||
export interface Logger extends BaseLogger {
|
||||
/**
|
||||
* Create a child logger.
|
||||
*
|
||||
* This child will use the `methodFactory` of the parent, so any log extensions applied to the parent
|
||||
* at the time of calling `getChild` will be applied to the child as well.
|
||||
* It will NOT apply changes to the parent's `methodFactory` after the child was created.
|
||||
* Those changes need to be applied to the child manually.
|
||||
*
|
||||
* @param namespace - name to add to the current logger to generate the child. Some implementations of `Logger`
|
||||
* use this as a prefix; others use a different mechanism.
|
||||
*/
|
||||
getChild(namespace: string): Logger;
|
||||
}
|
||||
|
||||
/** The basic interface for a logger which doesn't support children */
|
||||
export interface BaseLogger {
|
||||
/**
|
||||
* Output trace message to the logger, with stack trace.
|
||||
*
|
||||
* @param msg - Data to log.
|
||||
*/
|
||||
trace(this: void, ...msg: any[]): void;
|
||||
|
||||
/**
|
||||
* Output debug message to the logger.
|
||||
*
|
||||
* @param msg - Data to log.
|
||||
*/
|
||||
debug(this: void, ...msg: any[]): void;
|
||||
|
||||
/**
|
||||
* Output info message to the logger.
|
||||
*
|
||||
* @param msg - Data to log.
|
||||
*/
|
||||
info(this: void, ...msg: any[]): void;
|
||||
|
||||
/**
|
||||
* Output warn message to the logger.
|
||||
*
|
||||
* @param msg - Data to log.
|
||||
*/
|
||||
warn(this: void, ...msg: any[]): void;
|
||||
|
||||
/**
|
||||
* Output error message to the logger.
|
||||
*
|
||||
* @param msg - Data to log.
|
||||
*/
|
||||
error(this: void, ...msg: any[]): void;
|
||||
}
|
||||
import log, { Logger } from "loglevel";
|
||||
|
||||
// This is to demonstrate, that you can use any namespace you want.
|
||||
// Namespaces allow you to turn on/off the logging for specific parts of the
|
||||
@@ -96,11 +30,13 @@ const DEFAULT_NAMESPACE = "matrix";
|
||||
// to avoid the issue, we override the methodFactory of loglevel that binds to the
|
||||
// console methods at initialization time by a factory that looks up the console methods
|
||||
// when logging so we always get the current value of console methods.
|
||||
loglevel.methodFactory = function (methodName, logLevel, loggerName) {
|
||||
log.methodFactory = function (methodName, logLevel, loggerName) {
|
||||
return function (this: PrefixedLogger, ...args): void {
|
||||
/* eslint-disable @typescript-eslint/no-invalid-this */
|
||||
if (this.prefix) {
|
||||
args.unshift(this.prefix);
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-invalid-this */
|
||||
const supportedByConsole =
|
||||
methodName === "error" ||
|
||||
methodName === "warn" ||
|
||||
@@ -117,161 +53,34 @@ loglevel.methodFactory = function (methodName, logLevel, loggerName) {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Implementation of {@link Logger} based on `loglevel`.
|
||||
*/
|
||||
interface PrefixedLogger extends loglevel.Logger, LoggerWithLogMethod {
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal utility function: gets a {@link Logger} based on `loglevel`.
|
||||
*
|
||||
* Child loggers produced by {@link Logger.getChild} add the name of the child logger as a prefix on each log line.
|
||||
*
|
||||
* @param prefix Prefix to add to each logged line. If undefined, no prefix will be added.
|
||||
*/
|
||||
function getPrefixedLogger(prefix?: string): PrefixedLogger {
|
||||
const loggerName = DEFAULT_NAMESPACE + (prefix === undefined ? "" : `-${prefix}`);
|
||||
const prefixLogger = loglevel.getLogger(loggerName) as PrefixedLogger;
|
||||
|
||||
if (prefixLogger.getChild === undefined) {
|
||||
// This is a new loglevel Logger which has not been turned into a PrefixedLogger yet.
|
||||
prefixLogger.prefix = prefix;
|
||||
prefixLogger.getChild = (childPrefix): Logger => {
|
||||
// create the new child logger
|
||||
const childLogger = getPrefixedLogger((prefix ?? "") + childPrefix);
|
||||
// Assign the methodFactory from the parent logger.
|
||||
// This is useful if we add extensions to the parent logger that modifies
|
||||
// its methodFactory. (An example extension is: storing each log to a rageshake db)
|
||||
childLogger.methodFactory = prefixLogger.methodFactory;
|
||||
// Rebuild the child logger with the new methodFactory.
|
||||
childLogger.rebuild();
|
||||
return childLogger;
|
||||
};
|
||||
prefixLogger.setLevel(loglevel.levels.DEBUG, false);
|
||||
}
|
||||
|
||||
return prefixLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop-in replacement for `console` using {@link https://www.npmjs.com/package/loglevel|loglevel}.
|
||||
* Can be tailored down to specific use cases if needed.
|
||||
*
|
||||
* @deprecated avoid the use of this unless you are the constructor of `MatrixClient`: you should be using the logger
|
||||
* associated with `MatrixClient`.
|
||||
*/
|
||||
export const logger = getPrefixedLogger() as LoggerWithLogMethod;
|
||||
export const logger = log.getLogger(DEFAULT_NAMESPACE) as PrefixedLogger;
|
||||
logger.setLevel(log.levels.DEBUG, false);
|
||||
|
||||
/**
|
||||
* A "span" for grouping related log lines together.
|
||||
*
|
||||
* The current implementation just adds the name at the start of each log line.
|
||||
*
|
||||
* This offers a lighter-weight alternative to 'child' loggers returned by {@link Logger#getChild}. In particular,
|
||||
* it's not possible to apply individual filters to the LogSpan such as setting the verbosity level. On the other hand,
|
||||
* no reference to the LogSpan is retained in the logging framework, so it is safe to make lots of them over the course
|
||||
* of an application's life and just drop references to them when the job is done.
|
||||
*/
|
||||
export class LogSpan implements BaseLogger {
|
||||
private readonly name;
|
||||
|
||||
public constructor(
|
||||
private readonly parent: BaseLogger,
|
||||
name: string,
|
||||
) {
|
||||
this.name = name + ":";
|
||||
}
|
||||
|
||||
public trace(...msg: any[]): void {
|
||||
this.parent.trace(this.name, ...msg);
|
||||
}
|
||||
|
||||
public debug(...msg: any[]): void {
|
||||
this.parent.debug(this.name, ...msg);
|
||||
}
|
||||
|
||||
public info(...msg: any[]): void {
|
||||
this.parent.info(this.name, ...msg);
|
||||
}
|
||||
|
||||
public warn(...msg: any[]): void {
|
||||
this.parent.warn(this.name, ...msg);
|
||||
}
|
||||
|
||||
public error(...msg: any[]): void {
|
||||
this.parent.error(this.name, ...msg);
|
||||
}
|
||||
export interface PrefixedLogger extends Logger {
|
||||
withPrefix: (prefix: string) => PrefixedLogger;
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simplification of the `Debugger` type exposed by the `debug` library. We reimplement the bits we need here
|
||||
* to avoid a dependency on `debug`.
|
||||
*/
|
||||
interface Debugger {
|
||||
(formatter: any, ...args: any[]): void;
|
||||
extend: (namespace: string, delimiter?: string) => Debugger;
|
||||
function extendLogger(logger: Logger): void {
|
||||
(<PrefixedLogger>logger).withPrefix = function (prefix: string): PrefixedLogger {
|
||||
const existingPrefix = this.prefix || "";
|
||||
return getPrefixedLogger(existingPrefix + prefix);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A `Logger` instance, suitable for use in {@link ICreateClientOpts.logger}, which will write to the `debug` library.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* import debug from "debug";
|
||||
*
|
||||
* const client = createClient({
|
||||
* baseUrl: homeserverUrl,
|
||||
* userId: userId,
|
||||
* accessToken: "akjgkrgjs",
|
||||
* deviceId: "xzcvb",
|
||||
* logger: new DebugLogger(debug(`matrix-js-sdk:${userId}`)),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class DebugLogger implements Logger {
|
||||
public constructor(private debugInstance: Debugger) {}
|
||||
extendLogger(logger);
|
||||
|
||||
public trace(...msg: any[]): void {
|
||||
this.debugWithPrefix("[TRACE]", ...msg);
|
||||
}
|
||||
|
||||
public debug(...msg: any[]): void {
|
||||
this.debugWithPrefix("[DEBUG]", ...msg);
|
||||
}
|
||||
|
||||
public info(...msg: any[]): void {
|
||||
this.debugWithPrefix("[INFO]", ...msg);
|
||||
}
|
||||
|
||||
public warn(...msg: any[]): void {
|
||||
this.debugWithPrefix("[WARN]", ...msg);
|
||||
}
|
||||
|
||||
public error(...msg: any[]): void {
|
||||
this.debugWithPrefix("[ERROR]", ...msg);
|
||||
}
|
||||
|
||||
public getChild(namespace: string): DebugLogger {
|
||||
return new DebugLogger(this.debugInstance.extend(namespace));
|
||||
}
|
||||
|
||||
private debugWithPrefix(prefix: string, ...msg: any[]): void {
|
||||
let formatter: string;
|
||||
|
||||
// Convert the first argument to a string, so that we can safely add a prefix. This is much the same logic that
|
||||
// `debug()` uses.
|
||||
if (msg.length === 0) {
|
||||
formatter = "";
|
||||
} else if (msg[0] instanceof Error) {
|
||||
const err = msg.shift();
|
||||
formatter = err.stack || err.message;
|
||||
} else if (typeof msg[0] == "string") {
|
||||
formatter = msg.shift();
|
||||
} else {
|
||||
formatter = "%O";
|
||||
}
|
||||
this.debugInstance(prefix + " " + formatter, ...msg);
|
||||
function getPrefixedLogger(prefix: string): PrefixedLogger {
|
||||
const prefixLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`) as PrefixedLogger;
|
||||
if (prefixLogger.prefix !== prefix) {
|
||||
// Only do this setup work the first time through, as loggers are saved by name.
|
||||
extendLogger(prefixLogger);
|
||||
prefixLogger.prefix = prefix;
|
||||
prefixLogger.setLevel(log.levels.DEBUG, false);
|
||||
}
|
||||
return prefixLogger;
|
||||
}
|
||||
|
||||
186
node_modules/matrix-js-sdk/src/matrix.ts
generated
vendored
186
node_modules/matrix-js-sdk/src/matrix.ts
generated
vendored
@@ -14,104 +14,93 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type WidgetApi } from "matrix-widget-api";
|
||||
import { WidgetApi } from "matrix-widget-api";
|
||||
|
||||
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store.ts";
|
||||
import { MemoryStore } from "./store/memory.ts";
|
||||
import { MatrixScheduler } from "./scheduler.ts";
|
||||
import { MatrixClient, type ICreateClientOpts } from "./client.ts";
|
||||
import { RoomWidgetClient, type ICapabilities } from "./embedded.ts";
|
||||
import { type CryptoStore } from "./crypto/store/base.ts";
|
||||
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
|
||||
import { MemoryStore } from "./store/memory";
|
||||
import { MatrixScheduler } from "./scheduler";
|
||||
import { MatrixClient, ICreateClientOpts } from "./client";
|
||||
import { RoomWidgetClient, ICapabilities } from "./embedded";
|
||||
import { CryptoStore } from "./crypto/store/base";
|
||||
|
||||
export * from "./client.ts";
|
||||
export * from "./serverCapabilities.ts";
|
||||
export * from "./embedded.ts";
|
||||
export * from "./http-api/index.ts";
|
||||
export * from "./autodiscovery.ts";
|
||||
export * from "./sync-accumulator.ts";
|
||||
export * from "./errors.ts";
|
||||
export * from "./base64.ts";
|
||||
export * from "./models/beacon.ts";
|
||||
export * from "./models/event.ts";
|
||||
export * from "./models/room.ts";
|
||||
export * from "./models/event-timeline.ts";
|
||||
export * from "./models/event-timeline-set.ts";
|
||||
export * from "./models/poll.ts";
|
||||
export * from "./models/room-member.ts";
|
||||
export * from "./models/room-state.ts";
|
||||
export * from "./models/thread.ts";
|
||||
export * from "./models/typed-event-emitter.ts";
|
||||
export * from "./models/user.ts";
|
||||
export * from "./models/device.ts";
|
||||
export * from "./models/search-result.ts";
|
||||
export * from "./oauth/index.ts";
|
||||
export * from "./scheduler.ts";
|
||||
export * from "./filter.ts";
|
||||
export * from "./timeline-window.ts";
|
||||
export * from "./interactive-auth.ts";
|
||||
export * from "./version-support.ts";
|
||||
export * from "./service-types.ts";
|
||||
export * from "./store/memory.ts";
|
||||
export * from "./store/indexeddb.ts";
|
||||
export * from "./crypto/store/memory-crypto-store.ts";
|
||||
export * from "./crypto/store/localStorage-crypto-store.ts";
|
||||
export * from "./crypto/store/indexeddb-crypto-store.ts";
|
||||
export type { OutgoingRoomKeyRequest } from "./crypto/store/base.ts";
|
||||
export * from "./content-repo.ts";
|
||||
export type * from "./@types/common.ts";
|
||||
export type * from "./@types/uia.ts";
|
||||
export * from "./@types/event.ts";
|
||||
export * from "./@types/PushRules.ts";
|
||||
export * from "./@types/partials.ts";
|
||||
export * from "./@types/requests.ts";
|
||||
export * from "./@types/search.ts";
|
||||
export * from "./@types/beacon.ts";
|
||||
export * from "./@types/topic.ts";
|
||||
export * from "./@types/location.ts";
|
||||
export * from "./@types/threepids.ts";
|
||||
export * from "./@types/auth.ts";
|
||||
export * from "./@types/polls.ts";
|
||||
export * from "./@types/retention.ts";
|
||||
export type * from "./@types/local_notifications.ts";
|
||||
export type * from "./@types/registration.ts";
|
||||
export * from "./@types/read_receipts.ts";
|
||||
export type * from "./@types/crypto.ts";
|
||||
export * from "./@types/extensible_events.ts";
|
||||
export type * from "./@types/IIdentityServerProvider.ts";
|
||||
export * from "./@types/membership.ts";
|
||||
export * from "./models/room-summary.ts";
|
||||
export * from "./models/event-status.ts";
|
||||
export * from "./models/profile-keys.ts";
|
||||
export * from "./models/related-relations.ts";
|
||||
export { type StickyMatrixEvent, RoomStickyEventsEvent } from "./models/room-sticky-events.ts";
|
||||
export type { RoomSummary } from "./client.ts";
|
||||
export * as ContentHelpers from "./content-helpers.ts";
|
||||
export * as SecretStorage from "./secret-storage.ts";
|
||||
export { createNewMatrixCall, CallEvent } from "./webrtc/call.ts";
|
||||
export type { MatrixCall } from "./webrtc/call.ts";
|
||||
export * from "./client";
|
||||
export * from "./embedded";
|
||||
export * from "./http-api";
|
||||
export * from "./autodiscovery";
|
||||
export * from "./sync-accumulator";
|
||||
export * from "./errors";
|
||||
export * from "./models/beacon";
|
||||
export * from "./models/event";
|
||||
export * from "./models/room";
|
||||
export * from "./models/event-timeline";
|
||||
export * from "./models/event-timeline-set";
|
||||
export * from "./models/poll";
|
||||
export * from "./models/room-member";
|
||||
export * from "./models/room-state";
|
||||
export * from "./models/thread";
|
||||
export * from "./models/typed-event-emitter";
|
||||
export * from "./models/user";
|
||||
export * from "./models/device";
|
||||
export * from "./scheduler";
|
||||
export * from "./filter";
|
||||
export * from "./timeline-window";
|
||||
export * from "./interactive-auth";
|
||||
export * from "./service-types";
|
||||
export * from "./store/memory";
|
||||
export * from "./store/indexeddb";
|
||||
export * from "./crypto/store/memory-crypto-store";
|
||||
export * from "./crypto/store/indexeddb-crypto-store";
|
||||
export type { OutgoingRoomKeyRequest } from "./crypto/store/base";
|
||||
export * from "./content-repo";
|
||||
export * from "./@types/event";
|
||||
export * from "./@types/PushRules";
|
||||
export * from "./@types/partials";
|
||||
export * from "./@types/requests";
|
||||
export * from "./@types/search";
|
||||
export * from "./models/room-summary";
|
||||
export * as ContentHelpers from "./content-helpers";
|
||||
export * as SecretStorage from "./secret-storage";
|
||||
export type { ICryptoCallbacks } from "./crypto"; // used to be located here
|
||||
export { createNewMatrixCall } from "./webrtc/call";
|
||||
export type { MatrixCall } from "./webrtc/call";
|
||||
export {
|
||||
GroupCall,
|
||||
GroupCallEvent,
|
||||
GroupCallIntent,
|
||||
GroupCallState,
|
||||
GroupCallType,
|
||||
GroupCallStatsReportEvent,
|
||||
} from "./webrtc/groupCall.ts";
|
||||
} from "./webrtc/groupCall";
|
||||
export type { GroupCall } from "./webrtc/groupCall";
|
||||
export { CryptoEvent } from "./crypto";
|
||||
export { SlidingSyncEvent } from "./sliding-sync";
|
||||
export { MediaHandlerEvent } from "./webrtc/mediaHandler";
|
||||
export { CallEvent } from "./webrtc/call";
|
||||
export { CallFeedEvent } from "./webrtc/callFeed";
|
||||
export { StatsReport } from "./webrtc/stats/statsReport";
|
||||
export { RelationsEvent } from "./models/relations";
|
||||
export { LocalStorageErrors } from "./store/local-storage-events-emitter";
|
||||
|
||||
export { SyncState, SetPresence } from "./sync.ts";
|
||||
export type { ISyncStateData as SyncStateData } from "./sync.ts";
|
||||
export { SlidingSyncEvent } from "./sliding-sync.ts";
|
||||
export { MediaHandlerEvent } from "./webrtc/mediaHandler.ts";
|
||||
export { CallFeedEvent } from "./webrtc/callFeed.ts";
|
||||
export { StatsReport } from "./webrtc/stats/statsReport.ts";
|
||||
export { Relations, RelationsEvent } from "./models/relations.ts";
|
||||
export { TypedEventEmitter } from "./models/typed-event-emitter.ts";
|
||||
export { LocalStorageErrors, localStorageErrorsEventsEmitter } from "./store/local-storage-events-emitter.ts";
|
||||
export { IdentityProviderBrand, SSOAction } from "./@types/auth.ts";
|
||||
export type { ISSOFlow as SSOFlow, LoginFlow } from "./@types/auth.ts";
|
||||
export type { IHierarchyRelation as HierarchyRelation, IHierarchyRoom as HierarchyRoom } from "./@types/spaces.ts";
|
||||
export { LocationAssetType } from "./@types/location.ts";
|
||||
export { DebugLogger } from "./logger.ts";
|
||||
/**
|
||||
* Types supporting cryptography.
|
||||
*
|
||||
* The most important is {@link Crypto.CryptoApi}, an instance of which can be retrieved via
|
||||
* {@link MatrixClient.getCrypto}.
|
||||
*/
|
||||
export * as Crypto from "./crypto-api";
|
||||
|
||||
/**
|
||||
* Backwards compatibility re-export
|
||||
* @internal
|
||||
* @deprecated use {@link Crypto.CryptoApi}
|
||||
*/
|
||||
export type { CryptoApi } from "./crypto-api";
|
||||
|
||||
/**
|
||||
* Backwards compatibility re-export
|
||||
* @internal
|
||||
* @deprecated use {@link Crypto.DeviceVerificationStatus}
|
||||
*/
|
||||
export { DeviceVerificationStatus } from "./crypto-api";
|
||||
|
||||
let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore();
|
||||
|
||||
@@ -128,7 +117,7 @@ function amendClientOpts(opts: ICreateClientOpts): ICreateClientOpts {
|
||||
opts.store =
|
||||
opts.store ??
|
||||
new MemoryStore({
|
||||
localStorage: globalThis.localStorage,
|
||||
localStorage: global.localStorage,
|
||||
});
|
||||
opts.scheduler = opts.scheduler ?? new MatrixScheduler();
|
||||
opts.cryptoStore = opts.cryptoStore ?? cryptoStoreFactory();
|
||||
@@ -150,26 +139,11 @@ export function createClient(opts: ICreateClientOpts): MatrixClient {
|
||||
return new MatrixClient(amendClientOpts(opts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Matrix Client that works in a widget.
|
||||
* This client has a subset of features compared to a full client.
|
||||
* It uses the widget-api to communicate with matrix. (widget \<-\> client \<-\> homeserver)
|
||||
* @returns A new matrix client with a subset of features.
|
||||
* @param opts - The configuration options for this client. These configuration
|
||||
* options will be passed directly to {@link MatrixClient}.
|
||||
* @param widgetApi - The widget api to use for communication.
|
||||
* @param capabilities - The capabilities the widget client will request.
|
||||
* @param roomId - The room id the widget is associated with.
|
||||
* @param sendContentLoaded - Whether to send a content loaded widget action immediately after initial setup.
|
||||
* Set to `false` if the widget uses `waitForIFrameLoad=true` (in this case the client does not expect a content loaded action at all),
|
||||
* or if the the widget wants to send the `ContentLoaded` action at a later point in time after the initial setup.
|
||||
*/
|
||||
export function createRoomWidgetClient(
|
||||
widgetApi: WidgetApi,
|
||||
capabilities: ICapabilities,
|
||||
roomId: string,
|
||||
opts: ICreateClientOpts,
|
||||
sendContentLoaded = true,
|
||||
): MatrixClient {
|
||||
return new RoomWidgetClient(widgetApi, capabilities, roomId, amendClientOpts(opts), sendContentLoaded);
|
||||
return new RoomWidgetClient(widgetApi, capabilities, roomId, amendClientOpts(opts));
|
||||
}
|
||||
|
||||
438
node_modules/matrix-js-sdk/src/matrixrtc/CallMembership.ts
generated
vendored
438
node_modules/matrix-js-sdk/src/matrixrtc/CallMembership.ts
generated
vendored
@@ -1,438 +0,0 @@
|
||||
/*
|
||||
Copyright 2023-2026 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 { deepCompare } from "../utils.ts";
|
||||
import { type RTCCallIntent, type Transport, type SlotDescription } from "./types.ts";
|
||||
import { type MatrixEvent } from "../models/event.ts";
|
||||
import { type Logger, logger } from "../logger.ts";
|
||||
import { computeSlotId, slotIdToDescription } from "./utils.ts";
|
||||
import {
|
||||
checkRtcMembershipData,
|
||||
computeRtcIdentityRaw,
|
||||
type RtcMembershipData,
|
||||
checkSessionsMembershipData,
|
||||
type SessionMembershipData,
|
||||
MatrixRTCMembershipParseError,
|
||||
} from "./membershipData/index.ts";
|
||||
import { EventType } from "../@types/event.ts";
|
||||
|
||||
/**
|
||||
* The default duration in milliseconds that a membership is considered valid for.
|
||||
* Ordinarily the client responsible for the session will update the membership before it expires.
|
||||
* We use this duration as the fallback case where stale sessions are present for some reason.
|
||||
*/
|
||||
export const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4;
|
||||
|
||||
/**
|
||||
* Describes the source event type that provided the membership data.
|
||||
*/
|
||||
enum MembershipKind {
|
||||
/**
|
||||
* The modern MSC4143 format event.
|
||||
*/
|
||||
RTC = "rtc",
|
||||
/**
|
||||
* The legacy call event type.
|
||||
*/
|
||||
Session = "session",
|
||||
}
|
||||
|
||||
type MembershipData =
|
||||
| { kind: MembershipKind.RTC; data: RtcMembershipData }
|
||||
| { kind: MembershipKind.Session; data: SessionMembershipData };
|
||||
|
||||
type LimitedEvent = Pick<MatrixEvent, "getId" | "getSender" | "getTs" | "getType" | "getContent">;
|
||||
// TODO: Rename to RtcMembership once we removed the legacy SessionMembership is removed, to avoid confusion.
|
||||
export class CallMembership {
|
||||
/**
|
||||
* Parse the membershipdata from a call membership event.
|
||||
* @param matrixEvent The Matrix event to read.
|
||||
* @returns MembershipData in either MembershipKind.RTC or MembershipKind.Session format.
|
||||
* @throws If the content is neither format.
|
||||
*/
|
||||
public static membershipDataFromMatrixEvent(matrixEvent: LimitedEvent): MembershipData {
|
||||
const sender = matrixEvent.getSender();
|
||||
const evType = matrixEvent.getType();
|
||||
const data = matrixEvent.getContent();
|
||||
if (sender === undefined) throw new Error("matrixEvent is missing sender field");
|
||||
try {
|
||||
// Event types are strictly checked here.
|
||||
if (evType === EventType.RTCMembership && checkRtcMembershipData(data, sender)) {
|
||||
return { kind: MembershipKind.RTC, data };
|
||||
} else if (evType === EventType.GroupCallMemberPrefix && checkSessionsMembershipData(data)) {
|
||||
return { kind: MembershipKind.Session, data };
|
||||
} else {
|
||||
throw Error(`'${evType} is not a known call membership type`);
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex instanceof MatrixRTCMembershipParseError) {
|
||||
logger.debug("CallMembership.MatrixRTCMembershipParseError provided invalid data", data);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the contents of a MatrixEvent and create a CallMembership instance.
|
||||
* @param matrixEvent The Matrix event to read.
|
||||
*/
|
||||
public static async parseFromEvent(matrixEvent: LimitedEvent): Promise<CallMembership> {
|
||||
const membershipData: MembershipData = this.membershipDataFromMatrixEvent(matrixEvent);
|
||||
const rtcBackendIdentity =
|
||||
membershipData.kind === MembershipKind.RTC
|
||||
? await computeRtcIdentityRaw(
|
||||
membershipData.data.member.user_id,
|
||||
membershipData.data.member.device_id,
|
||||
membershipData.data.member.id,
|
||||
)
|
||||
: `${matrixEvent.getSender()}:${membershipData.data.device_id}`;
|
||||
return new CallMembership(matrixEvent, membershipData, rtcBackendIdentity);
|
||||
}
|
||||
|
||||
public static equal(a?: CallMembership, b?: CallMembership): boolean {
|
||||
return deepCompare(a?.membershipData, b?.membershipData);
|
||||
}
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
/** The parsed data from the Matrix event.
|
||||
* To access checked eventId and sender from the matrixEvent.
|
||||
* Class construction will fail if these values cannot get obtained. */
|
||||
private readonly matrixEventData: { eventId: string; sender: string };
|
||||
|
||||
/**
|
||||
* Use `parseFromEvent`.
|
||||
* Constructor should only be used by tests.
|
||||
* @private
|
||||
* @param matrixEvent
|
||||
* @param membershipData
|
||||
* @param rtcBackendIdentity
|
||||
*/
|
||||
public constructor(
|
||||
/** The Matrix event that this membership is based on */
|
||||
private readonly matrixEvent: LimitedEvent,
|
||||
private readonly membershipData: MembershipData,
|
||||
public readonly rtcBackendIdentity: string,
|
||||
) {
|
||||
const eventId = matrixEvent.getId();
|
||||
const sender = matrixEvent.getSender();
|
||||
|
||||
if (eventId === undefined) throw new Error("parentEvent is missing eventId field");
|
||||
if (sender === undefined) throw new Error("parentEvent is missing sender field");
|
||||
|
||||
this.logger = logger.getChild(`[CallMembership ${sender}:${this.deviceId}]`);
|
||||
this.matrixEventData = { eventId, sender };
|
||||
}
|
||||
|
||||
/** @deprecated use userId instead */
|
||||
public get sender(): string {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public get userId(): string {
|
||||
const { kind, data } = this.membershipData;
|
||||
switch (kind) {
|
||||
case MembershipKind.RTC:
|
||||
return data.member.user_id;
|
||||
case MembershipKind.Session:
|
||||
default:
|
||||
return this.matrixEventData.sender;
|
||||
}
|
||||
}
|
||||
|
||||
public get eventId(): string {
|
||||
return this.matrixEventData.eventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ID of the MatrixRTC slot that this membership belongs to (format `{application}#{id}`).
|
||||
* This is computed in case SessionMembershipData is used.
|
||||
*/
|
||||
public get slotId(): string {
|
||||
const { kind, data } = this.membershipData;
|
||||
if (data.application === "m.call") {
|
||||
switch (kind) {
|
||||
case MembershipKind.RTC:
|
||||
return data.slot_id;
|
||||
case MembershipKind.Session:
|
||||
default: {
|
||||
const [application, id] = [data.application, data.call_id];
|
||||
|
||||
// INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
|
||||
// The spec got changed to use `"ROOM"` instead of `""` empyt string for the implicit default call.
|
||||
// State events still are sent with `""` however. To find other events that should end up in the same call,
|
||||
// we use the slotId.
|
||||
// Since the CallMembership is the public representation of a rtc.member event, we just pretend it is a
|
||||
// "ROOM" slotId/call_id.
|
||||
// This makes all the remote members work with just this simple trick.
|
||||
//
|
||||
// We of course now need to be careful when sending legacy events (state events)
|
||||
// They get a slotDescription containing "ROOM" since this is what we use starting at the time this comment
|
||||
// is commited.
|
||||
//
|
||||
// See the Other INFO_SLOT_ID_LEGACY_CASE comments to see where we revert back to "" just before sending the event.
|
||||
let compatibilityAdaptedId: string;
|
||||
if (id === "") {
|
||||
compatibilityAdaptedId = "ROOM";
|
||||
this.logger?.info("use slotId compat hack emptyString -> ROOM");
|
||||
} else {
|
||||
compatibilityAdaptedId = id;
|
||||
}
|
||||
return computeSlotId({
|
||||
application,
|
||||
id: compatibilityAdaptedId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger?.info("NOT using slotId compat hack emptyString -> ROOM");
|
||||
// This is what the function should look like for any other application that did not
|
||||
// go through a `""`=> `"ROOM"` rename
|
||||
switch (kind) {
|
||||
case MembershipKind.RTC:
|
||||
return data.slot_id;
|
||||
case MembershipKind.Session:
|
||||
default:
|
||||
return computeSlotId({ application: data.application, id: data.call_id });
|
||||
}
|
||||
}
|
||||
|
||||
public get deviceId(): string {
|
||||
const { kind, data } = this.membershipData;
|
||||
switch (kind) {
|
||||
case MembershipKind.RTC:
|
||||
return data.member.device_id;
|
||||
case MembershipKind.Session:
|
||||
default:
|
||||
return data.device_id;
|
||||
}
|
||||
}
|
||||
|
||||
public get callIntent(): RTCCallIntent | undefined {
|
||||
const intent = this.applicationData["m.call.intent"];
|
||||
if (typeof intent === "string") {
|
||||
return intent;
|
||||
}
|
||||
this.logger.warn("RTC membership has invalid m.call.intent");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed `slot_id` (format `{application}#{id}`) into its components (application and id).
|
||||
*/
|
||||
public get slotDescription(): SlotDescription {
|
||||
const { kind, data } = this.membershipData;
|
||||
if (kind === MembershipKind.RTC) {
|
||||
const id = data.slot_id.slice(`${data.application.type}#`.length);
|
||||
return { application: data.application.type, id };
|
||||
}
|
||||
return slotIdToDescription(this.slotId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The application `type`.
|
||||
* @deprecated Use @see applicationData
|
||||
*/
|
||||
public get application(): string {
|
||||
return this.applicationData.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the application being used for the RTC session.
|
||||
* May contain extra keys specific to the application.
|
||||
*/
|
||||
public get applicationData(): { type: string; [key: string]: unknown } {
|
||||
const { kind, data } = this.membershipData;
|
||||
switch (kind) {
|
||||
case MembershipKind.RTC:
|
||||
return data.application;
|
||||
case MembershipKind.Session:
|
||||
default:
|
||||
// SessionData does not have application data as such. We return specific
|
||||
// properties in use by other getters in this class, for compatibility.
|
||||
return { "type": data.application, "m.call.intent": data["m.call.intent"] };
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated scope is not used and will be removed in future versions. replaced by application specific types.*/
|
||||
public get scope(): SessionMembershipData["scope"] | undefined {
|
||||
const { kind, data } = this.membershipData;
|
||||
switch (kind) {
|
||||
case MembershipKind.RTC:
|
||||
return undefined;
|
||||
case MembershipKind.Session:
|
||||
default:
|
||||
return data.scope;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This computes the membership ID for the membership.
|
||||
* For the sticky event based rtcSessionData this is trivial it is `member.id`.
|
||||
* This is not supposed to be used to identity on an rtc backend. This is just a nouance for
|
||||
* a generated (sha256) anonymised identity. Only send `rtcBackendIdentity` to any rtc backend service.
|
||||
*
|
||||
* For the legacy sessionMemberEvents it is a bit more complex. Here we sometimes do not have this data
|
||||
* in the event content and we expected the SFU and the client to use `${this.matrixEventData.sender}:${data.device_id}`.
|
||||
*
|
||||
* So if there is no membershipID we use the hard coded jwt id default (`${this.matrixEventData.sender}:${data.device_id}`)
|
||||
* value (used until version 0.16.0)
|
||||
*
|
||||
* It is also possible for a session event to set a custom membershipID. in that case this will be used.
|
||||
*/
|
||||
public get memberId(): string {
|
||||
// the createdTs behaves equivalent to the membershipID.
|
||||
// we only need the field for the legacy member events where we needed to update them
|
||||
// synapse ignores sending state events if they have the same content.
|
||||
const { kind, data } = this.membershipData;
|
||||
switch (kind) {
|
||||
case "rtc":
|
||||
return data.member.id;
|
||||
case "session":
|
||||
return (
|
||||
// best case we have a client already publishing the right custom membershipId
|
||||
data.membershipID ??
|
||||
// alternativly we use the hard coded jwt id defuatl value (used until version 0.16.0)
|
||||
`${this.matrixEventData.sender}:${data.device_id}`
|
||||
);
|
||||
default:
|
||||
throw Error("Not possible to get memberID without knowing the membership event kind");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated renamed to `memberId`
|
||||
*/
|
||||
public get membershipID(): string {
|
||||
return this.memberId;
|
||||
}
|
||||
|
||||
public createdTs(): number {
|
||||
const { kind, data } = this.membershipData;
|
||||
switch (kind) {
|
||||
case MembershipKind.RTC:
|
||||
// TODO we need to read the referenced (relation) event if available to get the real created_ts
|
||||
return this.matrixEvent.getTs();
|
||||
case MembershipKind.Session:
|
||||
default:
|
||||
return data.created_ts ?? this.matrixEvent.getTs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the absolute expiry timestamp of the membership.
|
||||
* @returns The absolute expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable
|
||||
*/
|
||||
public getAbsoluteExpiry(): number | undefined {
|
||||
const { kind, data } = this.membershipData;
|
||||
switch (kind) {
|
||||
case MembershipKind.RTC:
|
||||
return undefined;
|
||||
case MembershipKind.Session:
|
||||
default:
|
||||
// TODO: calculate this from the MatrixRTCSession join configuration directly
|
||||
return this.createdTs() + (data.expires ?? DEFAULT_EXPIRE_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The number of milliseconds until the membership expires or undefined if applicable
|
||||
* @deprecated Not used by RTC events.
|
||||
*/
|
||||
public getMsUntilExpiry(): number | undefined {
|
||||
const { kind } = this.membershipData;
|
||||
if (kind === MembershipKind.Session) {
|
||||
const absExpiry = this.getAbsoluteExpiry();
|
||||
if (absExpiry) {
|
||||
// Assume that local clock is sufficiently in sync with other clocks in the distributed system.
|
||||
// We used to try and adjust for the local clock being skewed, but there are cases where this is not accurate.
|
||||
// The current implementation allows for the local clock to be -infinity to +MatrixRTCSession.MEMBERSHIP_EXPIRY_TIME/2
|
||||
return absExpiry - Date.now();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the membership has expired, otherwise false
|
||||
*/
|
||||
public isExpired(): boolean {
|
||||
const { kind } = this.membershipData;
|
||||
switch (kind) {
|
||||
case MembershipKind.RTC:
|
||||
return false;
|
||||
case MembershipKind.Session:
|
||||
default:
|
||||
return this.getMsUntilExpiry()! <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ## RTC Membership
|
||||
* Gets the primary transport to use for this RTC membership (m.rtc.member).
|
||||
* This will return the primary transport that is used by this call membership to publish their media.
|
||||
* Directly relates to the `transports.published` field.
|
||||
*
|
||||
* ## Legacy session membership
|
||||
* In case of a legacy session membership (m.call.member) this will return the selected transport where
|
||||
* media is published. How this selection happens depends on the `focus_active` field of the session membership.
|
||||
* If the `focus_selection` is `oldest_membership` this will return the transport of the oldest membership
|
||||
* in the room (based on the `created_ts` field of the session membership).
|
||||
* If the `focus_selection` is `multi_sfu` it will return the first transport of the `foci_preferred` list.
|
||||
* (`multi_sfu` is equivalent to how `m.rtc.member` `transports.published` work).
|
||||
* @param oldestMembership For backwards compatibility with session membership (legacy). Unused in case of RTC membership.
|
||||
* Always required to make the consumer not care if it deals with RTC or session memberships.
|
||||
* @returns The transport this membership uses to publish media or undefined if no transport is available.
|
||||
*/
|
||||
public getTransport(oldestMembership: CallMembership): Transport | undefined {
|
||||
const { kind, data } = this.membershipData;
|
||||
switch (kind) {
|
||||
case MembershipKind.RTC:
|
||||
return data.transports.published[0];
|
||||
case MembershipKind.Session:
|
||||
switch (data.focus_active.focus_selection) {
|
||||
case "oldest_membership":
|
||||
if (CallMembership.equal(this, oldestMembership)) return data.foci_preferred[0];
|
||||
if (oldestMembership !== undefined) return oldestMembership.getTransport(oldestMembership);
|
||||
break;
|
||||
case "multi_sfu":
|
||||
return data.foci_preferred[0];
|
||||
default:
|
||||
// `focus_selection` not understood.
|
||||
return undefined;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of the `transports.published` field for RTC memberships (m.rtc.member).
|
||||
* Or the value of the `foci_preferred` field for legacy session memberships (m.call.member).
|
||||
*/
|
||||
public get transports(): Transport[] {
|
||||
const { kind, data } = this.membershipData;
|
||||
switch (kind) {
|
||||
case MembershipKind.RTC:
|
||||
return data.transports.published;
|
||||
case MembershipKind.Session:
|
||||
default:
|
||||
return data.foci_preferred;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
node_modules/matrix-js-sdk/src/matrixrtc/EncryptionManager.ts
generated
vendored
54
node_modules/matrix-js-sdk/src/matrixrtc/EncryptionManager.ts
generated
vendored
@@ -1,54 +0,0 @@
|
||||
import { type EncryptionConfig } from "./MatrixRTCSession.ts";
|
||||
import { type CallMembership } from "./CallMembership.ts";
|
||||
import { type EncryptionKeyMapKey } from "./types.ts";
|
||||
|
||||
/**
|
||||
* The string used for the keys in the the encryption key map.
|
||||
* `@bob:examle.org:DEVICEID(UUIDRANDOM_MEMBERID_RANDOMUUID)`
|
||||
*/
|
||||
export function getEncryptionKeyMapKey(membership: CallMembershipIdentityParts): EncryptionKeyMapKey {
|
||||
return `${membership.userId}:${membership.deviceId}(${membership.memberId})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface is for testing and for making it possible to interchange the encryption manager.
|
||||
* @internal
|
||||
*/
|
||||
export interface IEncryptionManager {
|
||||
/**
|
||||
* Joins the encryption manager with the provided configuration.
|
||||
*
|
||||
* @param joinConfig - The configuration for joining encryption, or undefined
|
||||
* if no specific configuration is provided.
|
||||
*/
|
||||
join(joinConfig: EncryptionConfig | undefined): void;
|
||||
|
||||
/**
|
||||
* Leaves the encryption manager, cleaning up any associated resources.
|
||||
*/
|
||||
leave(): void;
|
||||
|
||||
/**
|
||||
* Called from the MatrixRTCSession when the memberships in this session updated.
|
||||
*
|
||||
* @param oldMemberships - The previous state of call memberships before the update.
|
||||
*/
|
||||
onMembershipsUpdate(oldMemberships: CallMembership[]): void;
|
||||
|
||||
/**
|
||||
* Retrieves the encryption keys currently managed by the encryption manager.
|
||||
*
|
||||
* @returns A map of participant IDs to their encryption keys.
|
||||
*/
|
||||
getEncryptionKeys(): ReadonlyMap<
|
||||
EncryptionKeyMapKey,
|
||||
ReadonlyArray<{
|
||||
key: Uint8Array<ArrayBuffer>;
|
||||
keyIndex: number;
|
||||
membership: CallMembershipIdentityParts;
|
||||
rtcBackendIdentity: string;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
|
||||
export type CallMembershipIdentityParts = Pick<CallMembership, "userId" | "deviceId" | "memberId">;
|
||||
63
node_modules/matrix-js-sdk/src/matrixrtc/IKeyTransport.ts
generated
vendored
63
node_modules/matrix-js-sdk/src/matrixrtc/IKeyTransport.ts
generated
vendored
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 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 { type CallMembershipIdentityParts } from "./EncryptionManager.ts";
|
||||
import { type ParticipantDeviceInfo } from "./types.ts";
|
||||
|
||||
export enum KeyTransportEvents {
|
||||
ReceivedKeys = "received_keys",
|
||||
NotSupportedError = "not_supported_error",
|
||||
}
|
||||
|
||||
export type KeyTransportEventsHandlerMap = {
|
||||
[KeyTransportEvents.ReceivedKeys]: KeyTransportEventListener;
|
||||
[KeyTransportEvents.NotSupportedError]: () => void;
|
||||
};
|
||||
|
||||
export type KeyTransportEventListener = (
|
||||
membership: CallMembershipIdentityParts,
|
||||
keyBase64Encoded: string,
|
||||
index: number,
|
||||
timestamp: number,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Generic interface for the transport used to share room keys.
|
||||
* Keys can be shared using different transports, e.g. to-device messages or room messages.
|
||||
*/
|
||||
export interface IKeyTransport {
|
||||
/**
|
||||
* Sends the current user media key to the given members.
|
||||
* @param keyBase64Encoded
|
||||
* @param index
|
||||
* @param members - The participants that should get they key
|
||||
*/
|
||||
sendKey(keyBase64Encoded: string, index: number, members: ParticipantDeviceInfo[]): Promise<void>;
|
||||
|
||||
/** Subscribe to keys from this transport. */
|
||||
on(event: KeyTransportEvents.ReceivedKeys, listener: KeyTransportEventListener): this;
|
||||
/** Unsubscribe from keys from this transport. */
|
||||
off(event: KeyTransportEvents.ReceivedKeys, listener: KeyTransportEventListener): this;
|
||||
|
||||
/** Once start is called the underlying transport will subscribe to its transport system.
|
||||
* Before start is called this transport will not emit any events.
|
||||
*/
|
||||
start(): void;
|
||||
/** Once stop is called the underlying transport will unsubscribe from its transport system.
|
||||
* After stop is called this transport will not emit any events.
|
||||
*/
|
||||
stop(): void;
|
||||
}
|
||||
120
node_modules/matrix-js-sdk/src/matrixrtc/IMembershipManager.ts
generated
vendored
120
node_modules/matrix-js-sdk/src/matrixrtc/IMembershipManager.ts
generated
vendored
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 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 type { CallMembership } from "./CallMembership.ts";
|
||||
import type { RTCCallIntent, Status, Transport } from "./types.ts";
|
||||
import { type TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
|
||||
export enum MembershipManagerEvent {
|
||||
StatusChanged = "StatusChanged",
|
||||
/**
|
||||
* Emitted when the membership manager has not heard back from the server for the duration
|
||||
* of the delayed event and hence failed to restart the delayed event.
|
||||
* This means that the user is probably not joined anymore and the leave event was distributed to other session members.
|
||||
*/
|
||||
ProbablyLeft = "ProbablyLeft",
|
||||
/**
|
||||
* Once the membershipManger has aquired the a delay id (after sending the state event)
|
||||
* It will emit and share the delay id.
|
||||
*/
|
||||
DelayIdChanged = "DelayIdChanged",
|
||||
}
|
||||
|
||||
export type MembershipManagerEventHandlerMap = {
|
||||
[MembershipManagerEvent.StatusChanged]: (prefStatus: Status, newStatus: Status) => void;
|
||||
[MembershipManagerEvent.ProbablyLeft]: (probablyLeft: boolean) => void;
|
||||
[MembershipManagerEvent.DelayIdChanged]: (delayId: string | undefined) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* This interface defines what a MembershipManager uses and exposes.
|
||||
* This interface is what we use to write tests and allows changing the actual implementation
|
||||
* without breaking tests because of some internal method renaming.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export interface IMembershipManager extends TypedEventEmitter<
|
||||
MembershipManagerEvent,
|
||||
MembershipManagerEventHandlerMap
|
||||
> {
|
||||
/**
|
||||
* If we are trying to join, or have successfully joined the session.
|
||||
* It does not reflect if the room state is already configured to represent us being joined.
|
||||
* It only means that the Manager should be trying to connect or to disconnect running.
|
||||
* The Manager is still running right after isJoined becomes false to send the disconnect events.
|
||||
* @returns true if we intend to be participating in the MatrixRTC session
|
||||
* @deprecated This name is confusing and replaced by `isActivated()`. (Returns the same as `isActivated()`)
|
||||
*/
|
||||
isJoined(): boolean;
|
||||
/**
|
||||
* If the manager is activated. This means it tries to do its job to join the call, resend state events...
|
||||
* It does not imply that the room state is already configured to represent being joined.
|
||||
* It means that the Manager tries to connect or is connected. ("the manager is still active")
|
||||
* Once `leave()` is called the manager is not activated anymore but still running until `leave()` resolves.
|
||||
* @returns `true` if we intend to be participating in the MatrixRTC session
|
||||
*/
|
||||
isActivated(): boolean;
|
||||
/**
|
||||
* Get the actual connection status of the manager.
|
||||
*/
|
||||
get status(): Status;
|
||||
|
||||
/**
|
||||
* The Current own state event if the manger is connected.
|
||||
* `undefined` if not connected.
|
||||
*/
|
||||
get ownMembership(): CallMembership | undefined;
|
||||
|
||||
/**
|
||||
* If the membership manager has reason to believe that the hs sent a leave event
|
||||
* and as a consequence the current user is perceived as left for other session participants.
|
||||
*/
|
||||
get probablyLeft(): boolean;
|
||||
|
||||
/**
|
||||
* If the membership manager has reason to believe that the hs sent a leave event
|
||||
* and as a consequence the current user is perceived as left for other session participants.
|
||||
*/
|
||||
get delayId(): string | undefined;
|
||||
|
||||
/**
|
||||
* Start sending all necessary events to make this user participate in the RTC session.
|
||||
* @param fociPreferred the list of preferred foci to use in the joined RTC membership event.
|
||||
* If multiSfuFocus is set, this is only needed if this client wants to publish to multiple transports simultaneously.
|
||||
* @param multiSfuFocus the active focus to use in the joined RTC membership event. Setting this implies the
|
||||
* membership manager will operate in a multi-SFU connection mode. If `undefined`, an `oldest_membership`
|
||||
* transport selection will be used instead.
|
||||
* @throws can throw if it exceeds a configured maximum retry.
|
||||
*/
|
||||
join(fociPreferred: Transport[], multiSfuFocus?: Transport, onError?: (error: unknown) => void): void;
|
||||
/**
|
||||
* Send all necessary events to make this user leave the RTC session.
|
||||
* @param timeout the maximum duration in ms until the promise is forced to resolve.
|
||||
* @returns It resolves with true in case the leave was sent successfully.
|
||||
* It resolves with false in case we hit the timeout before sending successfully.
|
||||
*/
|
||||
leave(timeout?: number): Promise<boolean>;
|
||||
/**
|
||||
* Call this if the MatrixRTC session members have changed.
|
||||
*/
|
||||
onRTCSessionMemberUpdate(memberships: CallMembership[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update the intent of a membership on the call (e.g. user is now providing a video feed)
|
||||
* @param callIntent The new intent to set.
|
||||
*/
|
||||
updateCallIntent(callIntent: RTCCallIntent): Promise<void>;
|
||||
}
|
||||
46
node_modules/matrix-js-sdk/src/matrixrtc/LivekitTransport.ts
generated
vendored
46
node_modules/matrix-js-sdk/src/matrixrtc/LivekitTransport.ts
generated
vendored
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd
|
||||
|
||||
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 { type Transport } from "./types.ts";
|
||||
|
||||
export interface LivekitTransportConfig extends Transport {
|
||||
type: "livekit";
|
||||
livekit_service_url: string;
|
||||
}
|
||||
|
||||
export const isLivekitTransportConfig = (object: any): object is LivekitTransportConfig =>
|
||||
object.type === "livekit" && "livekit_service_url" in object;
|
||||
|
||||
export interface LivekitTransport extends LivekitTransportConfig {
|
||||
livekit_alias: string;
|
||||
}
|
||||
|
||||
export const isLivekitTransport = (object: any): object is LivekitTransport =>
|
||||
isLivekitTransportConfig(object) && "livekit_alias" in object;
|
||||
|
||||
/**
|
||||
* @deprecated this is just needed for the old focus active / focus fields of a call membership.
|
||||
* Not needed for new implementations.
|
||||
*/
|
||||
export interface LivekitFocusSelection extends Transport {
|
||||
type: "livekit";
|
||||
focus_selection: "oldest_membership" | "multi_sfu";
|
||||
}
|
||||
/**
|
||||
* @deprecated see LivekitFocusSelection
|
||||
*/
|
||||
export const isLivekitFocusSelection = (object: any): object is LivekitFocusSelection =>
|
||||
object.type === "livekit" && "focus_selection" in object;
|
||||
1010
node_modules/matrix-js-sdk/src/matrixrtc/MatrixRTCSession.ts
generated
vendored
1010
node_modules/matrix-js-sdk/src/matrixrtc/MatrixRTCSession.ts
generated
vendored
File diff suppressed because it is too large
Load Diff
189
node_modules/matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager.ts
generated
vendored
189
node_modules/matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager.ts
generated
vendored
@@ -1,189 +0,0 @@
|
||||
/*
|
||||
Copyright 2023-2026 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 { type Logger } from "../logger.ts";
|
||||
import { type MatrixClient, ClientEvent } from "../client.ts";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
import { type Room } from "../models/room.ts";
|
||||
import { RoomStateEvent } from "../models/room-state.ts";
|
||||
import { type MatrixEvent } from "../models/event.ts";
|
||||
import { MatrixRTCSession } from "./MatrixRTCSession.ts";
|
||||
import { EventType } from "../@types/event.ts";
|
||||
import { type RtcSlotEventContent, type SlotDescription } from "./types.ts";
|
||||
import { computeSlotId } from "./utils.ts";
|
||||
|
||||
export enum MatrixRTCSessionManagerEvents {
|
||||
// A member has joined the MatrixRTC session, creating an active session in a room where there wasn't previously
|
||||
SessionStarted = "session_started",
|
||||
// All participants have left a given MatrixRTC session.
|
||||
SessionEnded = "session_ended",
|
||||
}
|
||||
|
||||
type EventHandlerMap = {
|
||||
[MatrixRTCSessionManagerEvents.SessionStarted]: (roomId: string, session: MatrixRTCSession) => void;
|
||||
[MatrixRTCSessionManagerEvents.SessionEnded]: (roomId: string, session: MatrixRTCSession) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Holds all active MatrixRTC session objects and creates new ones as events arrive.
|
||||
* One `MatrixRTCSessionManager` is required for each MatrixRTC sessionDescription (application, session id) that the client wants to support.
|
||||
* If no application type is specified in the constructor, the default is "m.call".
|
||||
*
|
||||
* This interface is UNSTABLE and may change without warning.
|
||||
*/
|
||||
export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionManagerEvents, EventHandlerMap> {
|
||||
// All the room-scoped sessions we know about. This will include any where the app
|
||||
// has queried for the MatrixRTC sessions in a room, whether it's ever had any members
|
||||
// or not). We keep a (lazily created) session object for every room to ensure that there
|
||||
// is only ever one single room session object for any given room for the lifetime of the
|
||||
// client: that way there can never be any code holding onto a stale object that is no
|
||||
// longer the correct session object for the room.
|
||||
private roomSessions = new Map<string, MatrixRTCSession>();
|
||||
|
||||
private readonly logger: Logger;
|
||||
|
||||
public constructor(
|
||||
rootLogger: Logger,
|
||||
private client: MatrixClient,
|
||||
private readonly slotDescription: SlotDescription = { application: "m.call", id: "ROOM" }, // Default to the Matrix Call application
|
||||
) {
|
||||
super();
|
||||
this.logger = rootLogger.getChild(`[MatrixRTCSessionManager ${computeSlotId(slotDescription)}]`);
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
// We shouldn't need to null-check here, but matrix-client.spec.ts mocks getRooms
|
||||
// returning nothing, and breaks tests if you change it to return an empty array :'(
|
||||
for (const room of this.client.getRooms() ?? []) {
|
||||
const session = MatrixRTCSession.sessionForSlot(this.client, room, this.slotDescription);
|
||||
if (session.memberships.length > 0) {
|
||||
this.roomSessions.set(room.roomId, session);
|
||||
}
|
||||
}
|
||||
|
||||
this.client.on(ClientEvent.Room, this.onRoom);
|
||||
this.client.on(ClientEvent.Event, this.onEvent);
|
||||
this.client.on(RoomStateEvent.Events, this.onRoomState);
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
for (const sess of this.roomSessions.values()) {
|
||||
void sess.stop();
|
||||
}
|
||||
this.roomSessions.clear();
|
||||
|
||||
this.client.off(ClientEvent.Room, this.onRoom);
|
||||
this.client.off(ClientEvent.Event, this.onEvent);
|
||||
this.client.off(RoomStateEvent.Events, this.onRoomState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the main MatrixRTC session for a room, or undefined if there is
|
||||
* no current session
|
||||
*/
|
||||
public getActiveRoomSession(room: Room): MatrixRTCSession | undefined {
|
||||
return this.roomSessions.get(room.roomId)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current slot state event's content for the given room's session.
|
||||
*
|
||||
* @returns The slot event's content, or `undefined` if no slot event exists for the session.
|
||||
*/
|
||||
public getRtcSlot(room: Room): RtcSlotEventContent | undefined {
|
||||
return this.getRoomSession(room).getRtcSlot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given room's slot is closed.
|
||||
*
|
||||
* @returns `true` if the slot is closed, `false` if the slot is open or `undefined`
|
||||
* if no slot exists.
|
||||
*/
|
||||
public isSlotClosed(room: Room): boolean | undefined {
|
||||
return this.getRoomSession(room).isSlotClosed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the main MatrixRTC session for a room, returning an empty session
|
||||
* if no members are currently participating
|
||||
*/
|
||||
public getRoomSession(room: Room): MatrixRTCSession {
|
||||
if (!this.roomSessions.has(room.roomId)) {
|
||||
this.roomSessions.set(
|
||||
room.roomId,
|
||||
MatrixRTCSession.sessionForSlot(this.client, room, this.slotDescription),
|
||||
);
|
||||
}
|
||||
|
||||
return this.roomSessions.get(room.roomId)!;
|
||||
}
|
||||
|
||||
private onRoom = (room: Room): void => {
|
||||
void this.refreshRoom(room);
|
||||
};
|
||||
|
||||
private readonly onEvent = (event: MatrixEvent): void => {
|
||||
if (!event.unstableStickyExpiresAt) return; // Not sticky, not interested.
|
||||
|
||||
if (event.getType() !== EventType.RTCMembership) return;
|
||||
|
||||
const room = this.client.getRoom(event.getRoomId());
|
||||
if (!room) return;
|
||||
|
||||
void this.refreshRoom(room);
|
||||
};
|
||||
|
||||
private readonly onRoomState = (event: MatrixEvent): void => {
|
||||
if (event.getType() !== EventType.GroupCallMemberPrefix && event.getType() !== EventType.RTCSlot) {
|
||||
return;
|
||||
}
|
||||
const room = this.client.getRoom(event.getRoomId());
|
||||
if (!room) {
|
||||
this.logger.error(`Got room state event for unknown room ${event.getRoomId()}!`);
|
||||
return;
|
||||
}
|
||||
|
||||
void this.refreshRoom(room);
|
||||
};
|
||||
|
||||
private async refreshRoom(room: Room): Promise<void> {
|
||||
const isNewSession = !this.roomSessions.has(room.roomId);
|
||||
const session = this.getRoomSession(room);
|
||||
|
||||
const wasActiveAndKnown = session.memberships.length > 0 && !isNewSession;
|
||||
// This needs to be here and the event listener cannot be setup in the MatrixRTCSession,
|
||||
// because we need the update to happen between:
|
||||
// wasActiveAndKnown = session.memberships.length > 0 and
|
||||
// nowActive = session.memberships.length
|
||||
// Alternatively we would need to setup some event emission when the RTC session ended.
|
||||
// TODO we want to add the emission en session end. This makes the responsibility of the session manager more clear.
|
||||
|
||||
await session._onRTCSessionMemberUpdate().catch((error) => {
|
||||
this.logger.error(`Error updating RTC session members for ${room.roomId}: ${error}`);
|
||||
});
|
||||
|
||||
const nowActive = session.memberships.length > 0;
|
||||
|
||||
if (wasActiveAndKnown && !nowActive) {
|
||||
this.logger.trace(`Session ended for ${room.roomId} (${session.memberships.length} members)`);
|
||||
this.emit(MatrixRTCSessionManagerEvents.SessionEnded, room.roomId, this.roomSessions.get(room.roomId)!);
|
||||
} else if (!wasActiveAndKnown && nowActive) {
|
||||
this.logger.trace(`Session started for ${room.roomId} (${session.memberships.length} members)`);
|
||||
this.emit(MatrixRTCSessionManagerEvents.SessionStarted, room.roomId, this.roomSessions.get(room.roomId)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
1127
node_modules/matrix-js-sdk/src/matrixrtc/MembershipManager.ts
generated
vendored
1127
node_modules/matrix-js-sdk/src/matrixrtc/MembershipManager.ts
generated
vendored
File diff suppressed because it is too large
Load Diff
138
node_modules/matrix-js-sdk/src/matrixrtc/MembershipManagerActionScheduler.ts
generated
vendored
138
node_modules/matrix-js-sdk/src/matrixrtc/MembershipManagerActionScheduler.ts
generated
vendored
@@ -1,138 +0,0 @@
|
||||
import { type Logger, logger as rootLogger } from "../logger.ts";
|
||||
import { type EmptyObject } from "../matrix.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
import { MembershipActionType } from "./MembershipManager.ts";
|
||||
|
||||
/** @internal */
|
||||
export interface Action {
|
||||
/**
|
||||
* When this action should be executed
|
||||
*/
|
||||
ts: number;
|
||||
/**
|
||||
* The state of the different loops
|
||||
* can also be thought of as the type of the action
|
||||
*/
|
||||
type: MembershipActionType;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type ActionUpdate =
|
||||
| {
|
||||
/** Replace all existing scheduled actions with this new array */
|
||||
replace: Action[];
|
||||
}
|
||||
| {
|
||||
/** Add these actions to the existing scheduled actions */
|
||||
insert: Action[];
|
||||
}
|
||||
| EmptyObject;
|
||||
|
||||
/**
|
||||
* This scheduler tracks the state of the current membership participation
|
||||
* and runs one central timer that wakes up a handler callback with the correct action + state
|
||||
* whenever necessary.
|
||||
*
|
||||
* It can also be awakened whenever a new action is added which is
|
||||
* earlier then the current "next awake".
|
||||
* @internal
|
||||
*/
|
||||
export class ActionScheduler {
|
||||
private logger: Logger;
|
||||
/**
|
||||
* This is tracking the state of the scheduler loop.
|
||||
* Only used to prevent starting the loop twice.
|
||||
*/
|
||||
public running = false;
|
||||
|
||||
public constructor(
|
||||
/** This is the callback called for each scheduled action (`this.addAction()`) */
|
||||
private membershipLoopHandler: (type: MembershipActionType) => Promise<ActionUpdate>,
|
||||
parentLogger?: Logger,
|
||||
) {
|
||||
this.logger = (parentLogger ?? rootLogger).getChild(`[NewMembershipActionScheduler]`);
|
||||
}
|
||||
|
||||
// function for the wakeup mechanism (in case we add an action externally and need to leave the current sleep)
|
||||
private wakeup: (update: ActionUpdate) => void = (update: ActionUpdate): void => {
|
||||
this.logger.error("Cannot call wakeup before calling `startWithJoin()`");
|
||||
};
|
||||
private _actions: Action[] = [];
|
||||
public get actions(): Action[] {
|
||||
return this._actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* This starts the main loop of the membership manager that handles event sending, delayed event sending and delayed event restarting.
|
||||
* @param initialActions The initial actions the manager will start with. It should be enough to pass: DelayedLeaveActionType.Initial
|
||||
* @returns Promise that resolves once all actions have run and no more are scheduled.
|
||||
* @throws This throws an error if one of the actions throws.
|
||||
* In most other error cases the manager will try to handle any server errors by itself.
|
||||
*/
|
||||
public async startWithJoin(): Promise<void> {
|
||||
if (this.running) {
|
||||
this.logger.error("Cannot call startWithJoin() on NewMembershipActionScheduler while already running");
|
||||
return;
|
||||
}
|
||||
this.running = true;
|
||||
this._actions = [{ ts: Date.now(), type: MembershipActionType.SendDelayedEvent }];
|
||||
try {
|
||||
while (this._actions.length > 0) {
|
||||
// Sort so next (smallest ts) action is at the beginning
|
||||
this._actions.sort((a, b) => a.ts - b.ts);
|
||||
const nextAction = this._actions[0];
|
||||
let wakeupUpdate: ActionUpdate | undefined = undefined;
|
||||
|
||||
// while we await for the next action, wakeup has to resolve the wakeupPromise
|
||||
const wakeupPromise = new Promise<void>((resolve) => {
|
||||
this.wakeup = (update: ActionUpdate): void => {
|
||||
wakeupUpdate = update;
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
if (nextAction.ts > Date.now()) await Promise.race([wakeupPromise, sleep(nextAction.ts - Date.now())]);
|
||||
|
||||
let handlerResult: ActionUpdate = {};
|
||||
if (!wakeupUpdate) {
|
||||
this.logger.debug(
|
||||
`Current MembershipManager processing: ${nextAction.type}\nQueue:`,
|
||||
this._actions,
|
||||
`\nDate.now: "${Date.now()}`,
|
||||
);
|
||||
try {
|
||||
// `this.wakeup` can also be called and sets the `wakeupUpdate` object while we are in the handler.
|
||||
handlerResult = await this.membershipLoopHandler(nextAction.type);
|
||||
} catch (e) {
|
||||
// Preserve the original error as `cause`.
|
||||
throw new Error(`The MembershipManager shut down because of the end condition: ${e}`, {
|
||||
cause: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
// remove the processed action only after we are done processing
|
||||
this._actions.splice(0, 1);
|
||||
// The wakeupUpdate always wins since that is a direct external update.
|
||||
const actionUpdate = wakeupUpdate ?? handlerResult;
|
||||
|
||||
if ("replace" in actionUpdate) {
|
||||
this._actions = actionUpdate.replace;
|
||||
} else if ("insert" in actionUpdate) {
|
||||
this._actions.push(...actionUpdate.insert);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Set the rtc session running state since we cannot recover from here and the consumer user of the
|
||||
// MatrixRTCSession class needs to manually rejoin.
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
this.logger.debug("Leave MembershipManager ActionScheduler loop (no more actions)");
|
||||
}
|
||||
|
||||
public initiateJoin(): void {
|
||||
this.wakeup?.({ replace: [{ ts: Date.now(), type: MembershipActionType.SendDelayedEvent }] });
|
||||
}
|
||||
public initiateLeave(): void {
|
||||
this.wakeup?.({ replace: [{ ts: Date.now(), type: MembershipActionType.SendScheduledDelayedLeaveEvent }] });
|
||||
}
|
||||
}
|
||||
459
node_modules/matrix-js-sdk/src/matrixrtc/RTCEncryptionManager.ts
generated
vendored
459
node_modules/matrix-js-sdk/src/matrixrtc/RTCEncryptionManager.ts
generated
vendored
@@ -1,459 +0,0 @@
|
||||
/*
|
||||
Copyright 2025-2026 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 {
|
||||
type CallMembershipIdentityParts,
|
||||
getEncryptionKeyMapKey,
|
||||
type IEncryptionManager,
|
||||
} from "./EncryptionManager.ts";
|
||||
import { type EncryptionConfig, type MembershipConfig } from "./MatrixRTCSession.ts";
|
||||
import type { CallMembership } from "./CallMembership.ts";
|
||||
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
||||
import { type IKeyTransport, type KeyTransportEventListener, KeyTransportEvents } from "./IKeyTransport.ts";
|
||||
import { type Logger } from "../logger.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
import {
|
||||
type EncryptionKeyMapKey,
|
||||
type InboundEncryptionSession,
|
||||
type OutboundEncryptionSession,
|
||||
type ParticipantDeviceInfo,
|
||||
} from "./types.ts";
|
||||
import { OutdatedKeyFilter } from "./utils.ts";
|
||||
import { computeRtcIdentityRaw } from "./membershipData/rtc.ts";
|
||||
|
||||
/**
|
||||
* RTCEncryptionManager is used to manage the encryption keys for a call.
|
||||
*
|
||||
* It is responsible for distributing the keys to the other participants and rotating the keys if needed.
|
||||
*
|
||||
* This manager when used with to-device transport will share the existing key only to new joiners, and rotate
|
||||
* if there is a leaver.
|
||||
*
|
||||
* XXX In the future we want to distribute a ratcheted key not the current one for new joiners.
|
||||
*/
|
||||
export class RTCEncryptionManager implements IEncryptionManager {
|
||||
// This is a stop-gap solution for now. The preferred way to handle this case would be instead
|
||||
// to create a NoOpEncryptionManager that does nothing and use it for the session.
|
||||
// This will be done when removing the legacy EncryptionManager.
|
||||
private manageMediaKeys = false;
|
||||
|
||||
private useHashedRtcBackendIdentity = false;
|
||||
private ownRtcBackendIdentityCache: string | undefined;
|
||||
|
||||
/**
|
||||
* Store the key rings for each participant.
|
||||
* The encryption manager stores the keys because the application layer might not be ready yet to handle the keys.
|
||||
* The keys are stored and can be retrieved later when the application layer is ready {@link RTCEncryptionManager#getEncryptionKeys}.
|
||||
*/
|
||||
private readonly participantKeyRings = new Map<
|
||||
EncryptionKeyMapKey,
|
||||
Array<{
|
||||
key: Uint8Array<ArrayBuffer>;
|
||||
keyIndex: number;
|
||||
membership: CallMembershipIdentityParts;
|
||||
rtcBackendIdentity: string;
|
||||
}>
|
||||
>();
|
||||
|
||||
// The current per-sender media key for this device
|
||||
private outboundSession: OutboundEncryptionSession | null = null;
|
||||
|
||||
/**
|
||||
* Ensures that there is only one distribute operation at a time for that call.
|
||||
*/
|
||||
private currentKeyDistributionPromise: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* The time to wait before using the outbound session after it has been distributed.
|
||||
* This is to ensure that the key is delivered to all participants before it is used.
|
||||
* When creating the first key, this is set to 0 so that the key can be used immediately.
|
||||
*/
|
||||
private useKeyDelay = 5000;
|
||||
|
||||
/**
|
||||
* We want to avoid rolling out a new outbound key when the previous one was created less than `keyRotationGracePeriodMs` milliseconds ago.
|
||||
* This is to avoid expensive key rotations when users quickly join the call in a row.
|
||||
*
|
||||
* This must be higher than `useKeyDelay` to have an effect.
|
||||
* If it is lower, the current key will always be older than the grace period.
|
||||
* @private
|
||||
*/
|
||||
private keyRotationGracePeriodMs = 10_000;
|
||||
|
||||
/**
|
||||
* If a new key distribution is being requested while one is going on, we will set this flag to true.
|
||||
* This will ensure that a new round is started after the current one.
|
||||
* @private
|
||||
*/
|
||||
private needToEnsureKeyAgain = false;
|
||||
|
||||
/**
|
||||
* There is a possibility that keys arrive in the wrong order.
|
||||
* For example, after a quick join/leave/join, there will be 2 keys of index 0 distributed, and
|
||||
* if they are received in the wrong order, the stream won't be decryptable.
|
||||
* For that reason we keep a small buffer of keys for a limited time to disambiguate.
|
||||
* @private
|
||||
*/
|
||||
private keyBuffer = new OutdatedKeyFilter();
|
||||
|
||||
private logger: Logger | undefined = undefined;
|
||||
|
||||
private readonly rtcIdentityProvider: (userId: string, deviceId: string, memberId: string) => Promise<string>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param ownMembership - our own membership info
|
||||
* @param getMemberships - function to get current memberships
|
||||
* @param transport - key transport (room or to-device)
|
||||
* @param statistics - statistics collector
|
||||
* @param onEncryptionKeysChanged - callback to notify the media layer of new keys
|
||||
* @param parentLogger - optional parent logger
|
||||
* @param rtcBackendIdProvider - A function to compute the rtc backend identity, exposed for testing purposes
|
||||
*/
|
||||
public constructor(
|
||||
private readonly ownMembership: CallMembershipIdentityParts,
|
||||
private getMemberships: () => CallMembership[],
|
||||
private transport: IKeyTransport,
|
||||
// Callback to notify the media layer of new keys
|
||||
private onEncryptionKeysChanged: (
|
||||
keyBin: Uint8Array<ArrayBuffer>,
|
||||
encryptionKeyIndex: number,
|
||||
membership: CallMembershipIdentityParts,
|
||||
rtcBackendIdentity: string,
|
||||
) => void,
|
||||
parentLogger?: Logger,
|
||||
rtcBackendIdProvider?: (userId: string, deviceId: string, memberId: string) => Promise<string>,
|
||||
) {
|
||||
this.logger = parentLogger?.getChild(`[EncryptionManager]`);
|
||||
this.rtcIdentityProvider = rtcBackendIdProvider ?? computeRtcIdentityRaw;
|
||||
}
|
||||
|
||||
private async getOwnRtcBackendIdentity(): Promise<string> {
|
||||
if (this.ownRtcBackendIdentityCache) return this.ownRtcBackendIdentityCache;
|
||||
|
||||
if (this.useHashedRtcBackendIdentity) {
|
||||
const { userId, deviceId, memberId } = this.ownMembership;
|
||||
this.logger?.info(
|
||||
// If we see this log multiple times, we need to reconsider the precompute call of getOwnRtcBackendIdentity
|
||||
`Computing RTC backend identity for ${userId}:${deviceId}:${memberId} (SHOULD ONLY BE CALLED ONCE)`,
|
||||
);
|
||||
this.ownRtcBackendIdentityCache = await this.rtcIdentityProvider(userId, deviceId, memberId);
|
||||
} else {
|
||||
this.ownRtcBackendIdentityCache = `${this.ownMembership.userId}:${this.ownMembership.deviceId}`;
|
||||
}
|
||||
return this.ownRtcBackendIdentityCache;
|
||||
}
|
||||
|
||||
public getEncryptionKeys(): ReadonlyMap<
|
||||
EncryptionKeyMapKey,
|
||||
ReadonlyArray<{
|
||||
key: Uint8Array<ArrayBuffer>;
|
||||
keyIndex: number;
|
||||
membership: CallMembershipIdentityParts;
|
||||
rtcBackendIdentity: string;
|
||||
}>
|
||||
> {
|
||||
return new Map(this.participantKeyRings);
|
||||
}
|
||||
|
||||
private keysWithoutMatchingRTCMembership: Array<{
|
||||
key: Uint8Array<ArrayBuffer>;
|
||||
keyIndex: number;
|
||||
membership: CallMembershipIdentityParts;
|
||||
}> = [];
|
||||
|
||||
private checkKeysWithoutMatchingRTCMembership(): void {
|
||||
const keyInfoTemp = this.keysWithoutMatchingRTCMembership;
|
||||
this.keysWithoutMatchingRTCMembership = [];
|
||||
keyInfoTemp.forEach((keyInfo) => {
|
||||
this.addKeyToParticipant(keyInfo.key, keyInfo.keyIndex, keyInfo.membership);
|
||||
});
|
||||
}
|
||||
|
||||
private addKeyToParticipant(
|
||||
key: Uint8Array<ArrayBuffer>,
|
||||
keyIndex: number,
|
||||
membership: CallMembershipIdentityParts,
|
||||
): void {
|
||||
const knownRtcMembership = this.getMemberships();
|
||||
const fullMembership = knownRtcMembership.find(
|
||||
(member) => member.userId === membership.userId && member.deviceId === membership.deviceId,
|
||||
);
|
||||
if (!fullMembership) {
|
||||
this.logger?.info(
|
||||
`No matching RTC membership for key from ${membership.userId}:${membership.deviceId}, delaying key addition`,
|
||||
);
|
||||
this.keysWithoutMatchingRTCMembership.push({ key, keyIndex, membership });
|
||||
return;
|
||||
}
|
||||
this.addKeyToParticipantWithBackendIdentity(key, keyIndex, membership, fullMembership.rtcBackendIdentity);
|
||||
}
|
||||
|
||||
private addKeyToParticipantWithBackendIdentity(
|
||||
key: Uint8Array<ArrayBuffer>,
|
||||
keyIndex: number,
|
||||
membership: CallMembershipIdentityParts,
|
||||
rtcBackendIdentity: string,
|
||||
): void {
|
||||
const mapKey = getEncryptionKeyMapKey(membership);
|
||||
if (!this.participantKeyRings.has(mapKey)) {
|
||||
this.participantKeyRings.set(mapKey, []);
|
||||
}
|
||||
this.participantKeyRings.get(mapKey)!.push({ key, keyIndex, membership, rtcBackendIdentity });
|
||||
this.onEncryptionKeysChanged(key, keyIndex, membership, rtcBackendIdentity);
|
||||
}
|
||||
|
||||
public join(joinConfig: (EncryptionConfig & MembershipConfig) | undefined): void {
|
||||
this.manageMediaKeys = joinConfig?.manageMediaKeys ?? true; // default to true
|
||||
this.useHashedRtcBackendIdentity = joinConfig?.unstableSendStickyEvents ?? false;
|
||||
this.useKeyDelay = joinConfig?.useKeyDelay ?? 1000;
|
||||
this.keyRotationGracePeriodMs = joinConfig?.keyRotationGracePeriodMs ?? 10_000;
|
||||
|
||||
this.transport.on(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);
|
||||
void this.getOwnRtcBackendIdentity(); // precompute own identity
|
||||
|
||||
this.logger?.info(`Joining room`);
|
||||
this.transport.start();
|
||||
}
|
||||
|
||||
public leave(): void {
|
||||
this.transport.off(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);
|
||||
this.transport.stop();
|
||||
this.participantKeyRings.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Will ensure that a new key is distributed and used to encrypt our media.
|
||||
* If there is already a key distribution in progress, it will schedule a new distribution round just after the current one is completed.
|
||||
* If this function is called repeatedly while a distribution is in progress,
|
||||
* the calls will be coalesced to a single new distribution (that will start just after the current one has completed).
|
||||
*/
|
||||
private ensureKeyDistribution(): void {
|
||||
// `manageMediaKeys` is a stop-gap solution for now. The preferred way to handle this case would be instead
|
||||
// to create a NoOpEncryptionManager that does nothing and use it for the session.
|
||||
// This will be done when removing the legacy EncryptionManager.
|
||||
if (!this.manageMediaKeys) return;
|
||||
if (this.currentKeyDistributionPromise == null) {
|
||||
this.logger?.debug(`No active rollout, start a new one`);
|
||||
// start a rollout
|
||||
this.currentKeyDistributionPromise = this.rolloutOutboundKey().then(() => {
|
||||
this.logger?.debug(`Rollout completed`);
|
||||
this.currentKeyDistributionPromise = null;
|
||||
if (this.needToEnsureKeyAgain) {
|
||||
this.logger?.debug(`New Rollout needed`);
|
||||
this.needToEnsureKeyAgain = false;
|
||||
// rollout a new one
|
||||
this.ensureKeyDistribution();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// There is a rollout in progress, but a key rotation is requested (could be caused by a ownMembership change)
|
||||
// Remember that a new rotation is needed after the current one.
|
||||
this.logger?.debug(`Rollout in progress, a new rollout will be started after the current one`);
|
||||
this.needToEnsureKeyAgain = true;
|
||||
}
|
||||
}
|
||||
|
||||
public onNewKeyReceived: KeyTransportEventListener = (membership, keyBase64Encoded, index, timestamp) => {
|
||||
// `manageMediaKeys` is a stop-gap solution for now. The preferred way to handle this case would be instead
|
||||
// to create a NoOpEncryptionManager that does nothing and use it for the session.
|
||||
// This will be done when removing the legacy EncryptionManager.
|
||||
if (!this.manageMediaKeys) {
|
||||
this.logger?.warn(
|
||||
`Received key over transport ${membership.userId}:${membership.deviceId} at index ${index} but media keys are disabled`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.logger?.debug(`Received key over transport ${membership.userId}:${membership.deviceId} at index ${index}`);
|
||||
|
||||
// We received a new key, notify the video layer of this new key so that it can decrypt the frames properly.
|
||||
const keyBin = decodeBase64(keyBase64Encoded);
|
||||
const candidateInboundSession: InboundEncryptionSession = {
|
||||
key: keyBin,
|
||||
membership,
|
||||
keyIndex: index,
|
||||
creationTS: timestamp,
|
||||
};
|
||||
|
||||
const outdated = this.keyBuffer.isOutdated(membership, candidateInboundSession);
|
||||
if (!outdated) {
|
||||
this.addKeyToParticipant(
|
||||
candidateInboundSession.key,
|
||||
candidateInboundSession.keyIndex,
|
||||
candidateInboundSession.membership,
|
||||
);
|
||||
} else {
|
||||
this.logger?.info(
|
||||
`Received an out of order key for ${membership.userId}:${membership.deviceId}, dropping it`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the ownMembership of the call changes.
|
||||
* This encryption manager is very basic, it will rotate the key everytime this is called.
|
||||
* @param oldMemberships - This parameter is not used here, but it is kept for compatibility with the interface.
|
||||
*/
|
||||
public onMembershipsUpdate(oldMemberships: CallMembership[] = []): void {
|
||||
this.logger?.trace(`onMembershipsUpdate`);
|
||||
|
||||
// Ensure the key is distributed. This will be no-op if the key is already being distributed to everyone.
|
||||
// If there is an ongoing distribution, it will be completed before a new one is started.
|
||||
this.ensureKeyDistribution();
|
||||
// ensure key emission to the rtc backend
|
||||
this.checkKeysWithoutMatchingRTCMembership();
|
||||
}
|
||||
|
||||
private async rolloutOutboundKey(): Promise<void> {
|
||||
const isFirstKey = this.outboundSession == null;
|
||||
if (isFirstKey) {
|
||||
// create the first key
|
||||
const firstKey = {
|
||||
key: this.generateRandomKey(),
|
||||
creationTS: Date.now(),
|
||||
sharedWith: [],
|
||||
keyId: 0,
|
||||
};
|
||||
this.outboundSession = firstKey;
|
||||
this.addKeyToParticipantWithBackendIdentity(
|
||||
firstKey.key,
|
||||
firstKey.keyId,
|
||||
this.ownMembership,
|
||||
await this.getOwnRtcBackendIdentity(),
|
||||
);
|
||||
}
|
||||
// get current memberships
|
||||
const toShareWith: ParticipantDeviceInfo[] = this.getMemberships()
|
||||
.filter((membership) => {
|
||||
return membership.sender != undefined;
|
||||
})
|
||||
.map((membership) => {
|
||||
return {
|
||||
userId: membership.sender,
|
||||
deviceId: membership.deviceId,
|
||||
membershipTs: membership.createdTs(),
|
||||
};
|
||||
});
|
||||
|
||||
let alreadySharedWith = this.outboundSession?.sharedWith ?? [];
|
||||
|
||||
// Some users might have rotate their ownMembership event (formally called fingerprint) meaning they might have
|
||||
// clear their key. Reset the `alreadySharedWith` flag for them.
|
||||
alreadySharedWith = alreadySharedWith.filter(
|
||||
(x) =>
|
||||
// If there was a member with same userId and deviceId but different membershipTs, we need to clear it
|
||||
!toShareWith.some(
|
||||
(o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs != o.membershipTs,
|
||||
),
|
||||
);
|
||||
|
||||
const anyLeft = alreadySharedWith.filter(
|
||||
(x) =>
|
||||
!toShareWith.some(
|
||||
(o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs == o.membershipTs,
|
||||
),
|
||||
);
|
||||
const anyJoined = toShareWith.filter(
|
||||
(x) =>
|
||||
!alreadySharedWith.some(
|
||||
(o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs == o.membershipTs,
|
||||
),
|
||||
);
|
||||
|
||||
let toDistributeTo: ParticipantDeviceInfo[] = [];
|
||||
let outboundKey: OutboundEncryptionSession;
|
||||
let hasKeyChanged = false;
|
||||
if (anyLeft.length > 0) {
|
||||
// We need to rotate the key
|
||||
const newOutboundKey = this.createNewOutboundSession();
|
||||
hasKeyChanged = true;
|
||||
toDistributeTo = toShareWith;
|
||||
outboundKey = newOutboundKey;
|
||||
} else if (anyJoined.length > 0) {
|
||||
const now = Date.now();
|
||||
const keyAge = now - this.outboundSession!.creationTS;
|
||||
// If the current key is recently created (less than `keyRotationGracePeriodMs`), we can keep it and just distribute it to the new joiners.
|
||||
if (keyAge < this.keyRotationGracePeriodMs) {
|
||||
// keep the same key
|
||||
// XXX In the future we want to distribute a ratcheted key, not the current one
|
||||
this.logger?.debug(`New joiners detected, but the key is recent enough (age:${keyAge}), keeping it`);
|
||||
toDistributeTo = anyJoined;
|
||||
outboundKey = this.outboundSession!;
|
||||
} else {
|
||||
// We need to rotate the key
|
||||
this.logger?.debug(`New joiners detected, rotating the key`);
|
||||
const newOutboundKey = this.createNewOutboundSession();
|
||||
hasKeyChanged = true;
|
||||
toDistributeTo = toShareWith;
|
||||
outboundKey = newOutboundKey;
|
||||
}
|
||||
} else {
|
||||
// no changes
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger?.trace(`Sending key...`);
|
||||
await this.transport.sendKey(encodeBase64(outboundKey.key), outboundKey.keyId, toDistributeTo);
|
||||
outboundKey.sharedWith.push(...toDistributeTo);
|
||||
this.logger?.trace(
|
||||
`key index:${outboundKey.keyId} sent to ${outboundKey.sharedWith.map((m) => `${m.userId}:${m.deviceId}`).join(",")}`,
|
||||
);
|
||||
if (hasKeyChanged) {
|
||||
// Delay a bit before using this key
|
||||
// It is recommended not to start using a key immediately but instead wait for a short time to make sure it is delivered.
|
||||
this.logger?.trace(`Delay Rollout for key:${outboundKey.keyId}...`);
|
||||
await sleep(this.useKeyDelay);
|
||||
this.logger?.trace(`...Delayed rollout of index:${outboundKey.keyId} `);
|
||||
this.addKeyToParticipantWithBackendIdentity(
|
||||
outboundKey.key,
|
||||
outboundKey.keyId,
|
||||
this.ownMembership,
|
||||
await this.getOwnRtcBackendIdentity(),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger?.error(`Failed to rollout key`, err);
|
||||
}
|
||||
}
|
||||
|
||||
private createNewOutboundSession(): OutboundEncryptionSession {
|
||||
const newOutboundKey: OutboundEncryptionSession = {
|
||||
key: this.generateRandomKey(),
|
||||
creationTS: Date.now(),
|
||||
sharedWith: [],
|
||||
keyId: this.nextKeyIndex(),
|
||||
};
|
||||
|
||||
this.logger?.info(`creating new outbound key index:${newOutboundKey.keyId}`);
|
||||
// Set this new key as the current one
|
||||
this.outboundSession = newOutboundKey;
|
||||
return newOutboundKey;
|
||||
}
|
||||
|
||||
private nextKeyIndex(): number {
|
||||
if (this.outboundSession) {
|
||||
return (this.outboundSession.keyId + 1) % 256;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private generateRandomKey(): Uint8Array<ArrayBuffer> {
|
||||
const key = new Uint8Array(16);
|
||||
globalThis.crypto.getRandomValues(key);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
197
node_modules/matrix-js-sdk/src/matrixrtc/ToDeviceKeyTransport.ts
generated
vendored
197
node_modules/matrix-js-sdk/src/matrixrtc/ToDeviceKeyTransport.ts
generated
vendored
@@ -1,197 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 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 { type WidgetApiResponseError } from "matrix-widget-api";
|
||||
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
import { type IKeyTransport, KeyTransportEvents, type KeyTransportEventsHandlerMap } from "./IKeyTransport.ts";
|
||||
import { type Logger, logger as rootLogger } from "../logger.ts";
|
||||
import { type EncryptionKeysToDeviceEventContent, type ParticipantDeviceInfo, type Statistics } from "./types.ts";
|
||||
import { ClientEvent, type MatrixClient } from "../client.ts";
|
||||
import type { MatrixEvent } from "../models/event.ts";
|
||||
import { EventType } from "../@types/event.ts";
|
||||
import { type CallMembershipIdentityParts } from "./EncryptionManager.ts";
|
||||
|
||||
export class NotSupportedError extends Error {
|
||||
public constructor(message?: string) {
|
||||
super(message);
|
||||
}
|
||||
public get name(): string {
|
||||
return "NotSupportedError";
|
||||
}
|
||||
}
|
||||
/**
|
||||
* ToDeviceKeyTransport is used to send MatrixRTC keys to other devices using the
|
||||
* to-device CS-API.
|
||||
*/
|
||||
export class ToDeviceKeyTransport
|
||||
extends TypedEventEmitter<KeyTransportEvents, KeyTransportEventsHandlerMap>
|
||||
implements IKeyTransport
|
||||
{
|
||||
private logger: Logger = rootLogger;
|
||||
|
||||
public setParentLogger(parentLogger: Logger): void {
|
||||
this.logger = parentLogger.getChild(`[ToDeviceKeyTransport]`);
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private membership: CallMembershipIdentityParts,
|
||||
private roomId: string,
|
||||
private client: Pick<MatrixClient, "encryptAndSendToDevice" | "on" | "off">,
|
||||
private statistics: Statistics,
|
||||
parentLogger?: Logger,
|
||||
) {
|
||||
super();
|
||||
this.setParentLogger(parentLogger ?? rootLogger);
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
|
||||
}
|
||||
|
||||
public async sendKey(keyBase64Encoded: string, index: number, members: ParticipantDeviceInfo[]): Promise<void> {
|
||||
const content: EncryptionKeysToDeviceEventContent = {
|
||||
keys: {
|
||||
index: index,
|
||||
key: keyBase64Encoded,
|
||||
},
|
||||
room_id: this.roomId,
|
||||
member: {
|
||||
claimed_device_id: this.membership.deviceId,
|
||||
id: this.membership.memberId,
|
||||
},
|
||||
session: {
|
||||
call_id: "",
|
||||
application: "m.call",
|
||||
scope: "m.room",
|
||||
},
|
||||
sent_ts: Date.now(),
|
||||
};
|
||||
|
||||
const targets = members
|
||||
.map((member) => {
|
||||
return {
|
||||
userId: member.userId,
|
||||
deviceId: member.deviceId,
|
||||
};
|
||||
})
|
||||
// filter out me
|
||||
.filter(
|
||||
(member) => !(member.userId == this.membership.userId && member.deviceId == this.membership.deviceId),
|
||||
);
|
||||
|
||||
if (targets.length > 0) {
|
||||
await this.client
|
||||
.encryptAndSendToDevice(EventType.CallEncryptionKeysPrefix, targets, content)
|
||||
.catch((error: WidgetApiResponseError) => {
|
||||
const msg: string = error.message;
|
||||
// This is not ideal. We would want to have a custom error type for unsupported actions.
|
||||
// This is not part of the widget API spec. Since as of now there are only two implementations:
|
||||
// Rust SDK + JS-SDK, and the JS-SDK does support to-device sending, we can assume that
|
||||
// this is a widget driver issue error message.
|
||||
if (
|
||||
(msg.includes("unknown variant") && msg.includes("send_to_device")) ||
|
||||
msg.includes("not supported")
|
||||
) {
|
||||
throw new NotSupportedError("The widget driver does not support to-device encryption");
|
||||
}
|
||||
});
|
||||
this.statistics.counters.roomEventEncryptionKeysSent += 1;
|
||||
} else {
|
||||
this.logger.warn("No targets found for sending key");
|
||||
}
|
||||
}
|
||||
|
||||
private receiveCallKeyEvent(fromUser: string, content: EncryptionKeysToDeviceEventContent): void {
|
||||
// The event has already been validated at this point.
|
||||
|
||||
this.statistics.counters.roomEventEncryptionKeysReceived += 1;
|
||||
|
||||
// What is this, and why is it needed?
|
||||
// Also to device events do not have an origin server ts
|
||||
const now = Date.now();
|
||||
const age = now - (typeof content.sent_ts === "number" ? content.sent_ts : now);
|
||||
this.statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age;
|
||||
|
||||
const hardcodedMemberIdAlternative = `${fromUser}:${content.member.claimed_device_id}`;
|
||||
|
||||
this.emit(
|
||||
KeyTransportEvents.ReceivedKeys,
|
||||
// TODO userId this is claimed information, deviceId is claimed information
|
||||
{
|
||||
userId: fromUser,
|
||||
deviceId: content.member.claimed_device_id,
|
||||
memberId: content.member.id ?? hardcodedMemberIdAlternative,
|
||||
},
|
||||
content.keys.key,
|
||||
content.keys.index,
|
||||
now,
|
||||
);
|
||||
}
|
||||
|
||||
private onToDeviceEvent = (event: MatrixEvent): void => {
|
||||
if (event.getType() !== EventType.CallEncryptionKeysPrefix) {
|
||||
// Ignore this is not a call encryption event
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Not possible to check if the event is encrypted or not
|
||||
// see https://github.com/matrix-org/matrix-rust-sdk/issues/4883
|
||||
// if (evnt.getWireType() != EventType.RoomMessageEncrypted) {
|
||||
// // WARN: The call keys were sent in clear. Ignore them
|
||||
// logger.warn(`Call encryption keys sent in clear from: ${event.getSender()}`);
|
||||
// return;
|
||||
// }
|
||||
|
||||
const content = this.getValidEventContent(event);
|
||||
if (!content) return;
|
||||
|
||||
if (!event.getSender()) return;
|
||||
|
||||
this.receiveCallKeyEvent(event.getSender()!, content);
|
||||
};
|
||||
|
||||
private getValidEventContent(event: MatrixEvent): EncryptionKeysToDeviceEventContent | undefined {
|
||||
const content = event.getContent();
|
||||
const roomId = content.room_id;
|
||||
if (!roomId) {
|
||||
// Invalid event
|
||||
this.logger.warn("Malformed Event: invalid call encryption keys event, no roomId");
|
||||
return;
|
||||
}
|
||||
if (roomId !== this.roomId) {
|
||||
this.logger.warn("Malformed Event: Mismatch roomId");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.keys || !content.keys.key || typeof content.keys.index !== "number") {
|
||||
this.logger.warn("Malformed Event: Missing keys field");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.member || !content.member.claimed_device_id) {
|
||||
this.logger.warn("Malformed Event: Missing claimed_device_id");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO check for session related fields once the to-device encryption uses the new format.
|
||||
return content as EncryptionKeysToDeviceEventContent;
|
||||
}
|
||||
}
|
||||
24
node_modules/matrix-js-sdk/src/matrixrtc/index.ts
generated
vendored
24
node_modules/matrix-js-sdk/src/matrixrtc/index.ts
generated
vendored
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 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 * from "./CallMembership.ts";
|
||||
export * from "./LivekitTransport.ts";
|
||||
export * from "./MatrixRTCSession.ts";
|
||||
export * from "./MatrixRTCSessionManager.ts";
|
||||
export type * from "./types.ts";
|
||||
export { type SessionMembershipData, type RtcMembershipData } from "./membershipData/index.ts";
|
||||
export { Status, parseCallNotificationContent, isMyMembership, RTC_SLOT_ENCRYPTION_PER_MEMBER } from "./types.ts";
|
||||
export { MembershipManagerEvent } from "./IMembershipManager.ts";
|
||||
27
node_modules/matrix-js-sdk/src/matrixrtc/membershipData/common.ts
generated
vendored
27
node_modules/matrix-js-sdk/src/matrixrtc/membershipData/common.ts
generated
vendored
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Thrown when an event is not valid for use with MatrixRTC.
|
||||
*/
|
||||
export class MatrixRTCMembershipParseError extends AggregateError {
|
||||
public constructor(
|
||||
public readonly type: string,
|
||||
errors: string[],
|
||||
) {
|
||||
super(errors, `Does not match ${type}:\n${errors.join("\n")}`);
|
||||
}
|
||||
}
|
||||
19
node_modules/matrix-js-sdk/src/matrixrtc/membershipData/index.ts
generated
vendored
19
node_modules/matrix-js-sdk/src/matrixrtc/membershipData/index.ts
generated
vendored
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 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 { type SessionMembershipData, checkSessionsMembershipData } from "./session.ts";
|
||||
export { type RtcMembershipData, computeRtcIdentityRaw, checkRtcMembershipData } from "./rtc.ts";
|
||||
export { MatrixRTCMembershipParseError } from "./common.ts";
|
||||
168
node_modules/matrix-js-sdk/src/matrixrtc/membershipData/rtc.ts
generated
vendored
168
node_modules/matrix-js-sdk/src/matrixrtc/membershipData/rtc.ts
generated
vendored
@@ -1,168 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 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 { MXID_PATTERN } from "../../models/room-member.ts";
|
||||
import type { IContent } from "../../models/event.ts";
|
||||
import type { RelationType } from "../../types.ts";
|
||||
import { type RtcSlotApplicationContent, type Transport } from "../types.ts";
|
||||
import { MatrixRTCMembershipParseError } from "./common.ts";
|
||||
import { sha256 } from "../../digest.ts";
|
||||
import { encodeUnpaddedBase64 } from "../../base64.ts";
|
||||
import { slotIdToDescription } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* Represents the current form of MSC4143, which uses sticky events to store membership.
|
||||
*/
|
||||
export interface RtcMembershipData {
|
||||
"slot_id": string;
|
||||
"member": {
|
||||
user_id: string;
|
||||
device_id: string;
|
||||
id: string;
|
||||
};
|
||||
"m.relates_to"?: {
|
||||
event_id: string;
|
||||
rel_type: RelationType.Reference;
|
||||
};
|
||||
"application": RtcSlotApplicationContent;
|
||||
"transports": {
|
||||
published: Transport[];
|
||||
can_subscribe: string[];
|
||||
};
|
||||
"versions": string[];
|
||||
"msc4354_sticky_key"?: string;
|
||||
"sticky_key"?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that `data` matches the format expected by MSC4143.
|
||||
* @param data The event content.
|
||||
* @param sender The sender of the event.
|
||||
* @returns true if `data` is valid RtcMembershipData
|
||||
* @throws {MatrixRTCMembershipParseError} if the content is not valid
|
||||
*/
|
||||
export const checkRtcMembershipData = (data: IContent, sender: string): data is RtcMembershipData => {
|
||||
const errors: string[] = [];
|
||||
const prefix = " - ";
|
||||
const expectedSlotPrefix = `${data?.application?.type}#`;
|
||||
|
||||
// required fields
|
||||
if (typeof data.slot_id !== "string") {
|
||||
errors.push(prefix + "slot_id must be string");
|
||||
} else if (!data.slot_id.startsWith(expectedSlotPrefix)) {
|
||||
errors.push(prefix + `slot_id must start with ${expectedSlotPrefix}`);
|
||||
} else {
|
||||
try {
|
||||
slotIdToDescription(data.slot_id);
|
||||
} catch (ex) {
|
||||
errors.push(prefix + `slot_id was badly formed${ex instanceof Error ? `: ${ex.message}` : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data.member !== "object" || data.member === null) {
|
||||
errors.push(prefix + "member must be an object");
|
||||
} else {
|
||||
if (typeof data.member.user_id !== "string") {
|
||||
errors.push(prefix + "member.user_id must be string");
|
||||
} else if (!MXID_PATTERN.test(data.member.user_id)) {
|
||||
errors.push(prefix + "member.user_id must be a valid mxid");
|
||||
}
|
||||
// This is not what the spec enforces but there currently are no rules what power levels are required to
|
||||
// send a m.rtc.member event for a other user. So we add this check for simplicity and to avoid possible attacks until there
|
||||
// is a proper definition when this is allowed.
|
||||
else if (data.member.user_id !== sender) {
|
||||
errors.push(prefix + "member.user_id must match the sender");
|
||||
}
|
||||
if (typeof data.member.device_id !== "string") {
|
||||
errors.push(prefix + "member.device_id must be string");
|
||||
}
|
||||
if (typeof data.member.id !== "string") errors.push(prefix + "member.id must be string");
|
||||
}
|
||||
if (typeof data.application !== "object" || data.application === null) {
|
||||
errors.push(prefix + "application must be an object");
|
||||
} else {
|
||||
if (typeof data.application.type !== "string") {
|
||||
errors.push(prefix + "application.type must be a string");
|
||||
} else {
|
||||
if (data.application.type.includes("#")) errors.push(prefix + 'application.type must not include "#"');
|
||||
}
|
||||
}
|
||||
if (typeof data.transports !== "object" || data.transports === null || !Array.isArray(data.transports.published)) {
|
||||
errors.push(prefix + "transports.published must be an array");
|
||||
} else {
|
||||
// validate that each transport has at least a string 'type'
|
||||
for (const t of data.transports.published) {
|
||||
if (typeof t !== "object" || t === null || typeof t.type !== "string") {
|
||||
errors.push(prefix + "transports.published entries must be objects with a string type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
typeof data.transports !== "object" ||
|
||||
data.transports === null ||
|
||||
!Array.isArray(data.transports.can_subscribe) ||
|
||||
!data.transports.can_subscribe.every((t: unknown) => typeof t === "string")
|
||||
) {
|
||||
errors.push(prefix + "transports.can_subscribe must be an array of strings");
|
||||
}
|
||||
if (data.versions === undefined || !Array.isArray(data.versions)) {
|
||||
errors.push(prefix + "versions must be an array");
|
||||
} else if (!data.versions.every((v) => typeof v === "string")) {
|
||||
errors.push(prefix + "versions must be an array of strings");
|
||||
}
|
||||
|
||||
// optional fields
|
||||
if ((data.sticky_key ?? data.msc4354_sticky_key) === undefined) {
|
||||
errors.push(prefix + "sticky_key or msc4354_sticky_key must be a defined");
|
||||
}
|
||||
if (data.sticky_key !== undefined && typeof data.sticky_key !== "string") {
|
||||
errors.push(prefix + "sticky_key must be a string");
|
||||
}
|
||||
if (data.msc4354_sticky_key !== undefined && typeof data.msc4354_sticky_key !== "string") {
|
||||
errors.push(prefix + "msc4354_sticky_key must be a string");
|
||||
}
|
||||
if (
|
||||
data.sticky_key !== undefined &&
|
||||
data.msc4354_sticky_key !== undefined &&
|
||||
data.sticky_key !== data.msc4354_sticky_key
|
||||
) {
|
||||
errors.push(prefix + "sticky_key and msc4354_sticky_key must be equal if both are defined");
|
||||
}
|
||||
if (data["m.relates_to"] !== undefined) {
|
||||
const rel = data["m.relates_to"] as RtcMembershipData["m.relates_to"];
|
||||
if (typeof rel !== "object" || rel === null) {
|
||||
errors.push(prefix + "m.relates_to must be an object if provided");
|
||||
} else {
|
||||
if (typeof rel.event_id !== "string") errors.push(prefix + "m.relates_to.event_id must be a string");
|
||||
if (rel.rel_type !== "m.reference") errors.push(prefix + "m.relates_to.rel_type must be m.reference");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
throw new MatrixRTCMembershipParseError("RtcMembership", errors);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export async function computeRtcIdentityRaw(userId: string, deviceId: string, memberId: string): Promise<string> {
|
||||
// canonical JSON serialization (Matrix canonical JSON for arrays)
|
||||
const jsonStr = JSON.stringify([userId, deviceId, memberId]);
|
||||
const hashBuffer = await sha256(jsonStr);
|
||||
const hashedString = encodeUnpaddedBase64(hashBuffer);
|
||||
return hashedString;
|
||||
}
|
||||
146
node_modules/matrix-js-sdk/src/matrixrtc/membershipData/session.ts
generated
vendored
146
node_modules/matrix-js-sdk/src/matrixrtc/membershipData/session.ts
generated
vendored
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 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 { type IContent } from "../../matrix.ts";
|
||||
import { type RTCCallIntent, type Transport } from "../types.ts";
|
||||
import { MatrixRTCMembershipParseError } from "./common.ts";
|
||||
|
||||
/**
|
||||
* (MatrixRTC) session membership data.
|
||||
* This represents the *OLD* form of MSC4143, which uses state events to store membership.
|
||||
* Represents the `session` in the memberships section of an m.call.member event as it is on the wire.
|
||||
**/
|
||||
export type SessionMembershipData = {
|
||||
/**
|
||||
* The RTC application defines the type of the RTC session.
|
||||
*/
|
||||
"application": string;
|
||||
|
||||
/**
|
||||
* The id of this session.
|
||||
* A session can never span over multiple rooms so this id is to distinguish between
|
||||
* multiple session in one room. A room wide session that is not associated with a user,
|
||||
* and therefore immune to creation race conflicts, uses the `call_id: ""`.
|
||||
*/
|
||||
"call_id": string;
|
||||
|
||||
/**
|
||||
* The Matrix device ID of this session. A single user can have multiple sessions on different devices.
|
||||
*/
|
||||
"device_id": string;
|
||||
|
||||
/**
|
||||
* The focus selection system this user/membership is using.
|
||||
* NOTE: This is still included for legacy reasons, but not consumed by the SDK.
|
||||
*/
|
||||
"focus_active": {
|
||||
type: "livekit" | string;
|
||||
focus_selection: "oldest_membership" | "multi_sfu" | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A list of possible foci this user knows about. One of them might be used based on the focus_active
|
||||
* selection system.
|
||||
*/
|
||||
"foci_preferred": Transport[];
|
||||
|
||||
/**
|
||||
* Optional field that contains the creation of the session. If it is undefined the creation
|
||||
* is the `origin_server_ts` of the event itself. For updates to the event this property tracks
|
||||
* the `origin_server_ts` of the initial join event.
|
||||
* - If it is undefined it can be interpreted as a "Join".
|
||||
* - If it is defined it can be interpreted as an "Update"
|
||||
*/
|
||||
"created_ts"?: number;
|
||||
|
||||
// Application specific data
|
||||
|
||||
/**
|
||||
* If the `application` = `"m.call"` this defines if it is a room or user owned call.
|
||||
* There can always be one room scoped call but multiple user owned calls (breakout sessions)
|
||||
*/
|
||||
"scope"?: "m.room" | "m.user";
|
||||
|
||||
/**
|
||||
* Optionally we allow to define a delta to the `created_ts` that defines when the event is expired/invalid.
|
||||
* This should be set to multiple hours. The only reason it exist is to deal with failed delayed events.
|
||||
* (for example caused by a homeserver crashes)
|
||||
**/
|
||||
"expires"?: number;
|
||||
|
||||
/**
|
||||
* The intent of the call from the perspective of this user. This may be an audio call, video call or
|
||||
* something else.
|
||||
*/
|
||||
"m.call.intent"?: RTCCallIntent;
|
||||
|
||||
/**
|
||||
* The id used on the media backend.
|
||||
* (With livekit this is the participant identity on the LK SFU)
|
||||
* This can be a UUID but right now it is `${this.matrixEventData.sender}:${data.device_id}`.
|
||||
*
|
||||
* It is compleatly valid to not set this field. Other clients will treat `undefined` as `${this.matrixEventData.sender}:${data.device_id}`
|
||||
*/
|
||||
"membershipID"?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that `data` matches the format expected by the legacy form of MSC4143.
|
||||
* @param data The event content.
|
||||
* @returns true if `data` is valid SessionMembershipData
|
||||
* @throws {MatrixRTCMembershipParseError} if the content is not valid
|
||||
*/
|
||||
export const checkSessionsMembershipData = (data: IContent): data is SessionMembershipData => {
|
||||
const prefix = " - ";
|
||||
const errors: string[] = [];
|
||||
if (typeof data.device_id !== "string") errors.push(prefix + "device_id must be string");
|
||||
if (typeof data.call_id !== "string") errors.push(prefix + "call_id must be string");
|
||||
if (typeof data.application !== "string") errors.push(prefix + "application must be a string");
|
||||
if (data.focus_active === undefined) {
|
||||
errors.push(prefix + "focus_active has an invalid type");
|
||||
}
|
||||
if (typeof data.focus_active?.type !== "string") {
|
||||
errors.push(prefix + "focus_active.type must be a string");
|
||||
}
|
||||
if (
|
||||
data.foci_preferred !== undefined &&
|
||||
!(
|
||||
Array.isArray(data.foci_preferred) &&
|
||||
data.foci_preferred.every(
|
||||
(f: Transport) => typeof f === "object" && f !== null && typeof f.type === "string",
|
||||
)
|
||||
)
|
||||
) {
|
||||
errors.push(prefix + "foci_preferred must be an array of transport objects");
|
||||
}
|
||||
// optional parameters
|
||||
if (data.created_ts !== undefined && typeof data.created_ts !== "number") {
|
||||
errors.push(prefix + "created_ts must be number");
|
||||
}
|
||||
|
||||
// application specific data (we first need to check if they exist)
|
||||
if (data.scope !== undefined && typeof data.scope !== "string") errors.push(prefix + "scope must be string");
|
||||
|
||||
if (data["m.call.intent"] !== undefined && typeof data["m.call.intent"] !== "string") {
|
||||
errors.push(prefix + "m.call.intent must be a string");
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
throw new MatrixRTCMembershipParseError("SessionMembership", errors);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
244
node_modules/matrix-js-sdk/src/matrixrtc/types.ts
generated
vendored
244
node_modules/matrix-js-sdk/src/matrixrtc/types.ts
generated
vendored
@@ -1,244 +0,0 @@
|
||||
/*
|
||||
Copyright 2023-2026 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 type { IContent, IMentions } from "../matrix.ts";
|
||||
import type { RelationEvent } from "../types.ts";
|
||||
import type { CallMembership } from "./CallMembership.ts";
|
||||
import { type CallMembershipIdentityParts } from "./EncryptionManager.ts";
|
||||
|
||||
export type EncryptionKeyMapKey = string;
|
||||
|
||||
export interface EncryptionKeyEntry {
|
||||
index: number;
|
||||
key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The mxID, deviceId and membership timestamp of a RTC session participant.
|
||||
*/
|
||||
export type ParticipantDeviceInfo = {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
membershipTs: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A type representing the information needed to decrypt video streams.
|
||||
*/
|
||||
export type InboundEncryptionSession = {
|
||||
key: Uint8Array<ArrayBuffer>;
|
||||
membership: CallMembershipIdentityParts;
|
||||
keyIndex: number;
|
||||
creationTS: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The information about the key used to encrypt video streams.
|
||||
*/
|
||||
export type OutboundEncryptionSession = {
|
||||
key: Uint8Array<ArrayBuffer>;
|
||||
creationTS: number;
|
||||
// The devices that this key is shared with.
|
||||
sharedWith: Array<ParticipantDeviceInfo>;
|
||||
// This is an index acting as the id of the key
|
||||
keyId: number;
|
||||
};
|
||||
|
||||
export interface EncryptionKeysEventContent {
|
||||
keys: EncryptionKeyEntry[];
|
||||
device_id: string;
|
||||
call_id: string;
|
||||
sent_ts?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* THe content of a to-device event that contains encryption keys.
|
||||
*/
|
||||
export interface EncryptionKeysToDeviceEventContent {
|
||||
keys: { index: number; key: string };
|
||||
member: {
|
||||
id: string;
|
||||
// TODO Remove that it is claimed, need to get the sealed sender from decryption info
|
||||
// Or add some validation on it based on the encryption info
|
||||
claimed_device_id: string;
|
||||
};
|
||||
room_id: string;
|
||||
session: {
|
||||
application: string;
|
||||
call_id: string;
|
||||
scope: string;
|
||||
};
|
||||
// Why is this needed?
|
||||
sent_ts?: number;
|
||||
}
|
||||
/**
|
||||
* @deprecated Use `RTCNotificationType` instead.
|
||||
*/
|
||||
export type CallNotifyType = "ring" | "notify";
|
||||
/**
|
||||
* @deprecated Use `IRTCNotificationContent` instead.
|
||||
*/
|
||||
export interface ICallNotifyContent {
|
||||
"application": string;
|
||||
"m.mentions": IMentions;
|
||||
"notify_type": CallNotifyType;
|
||||
"call_id": string;
|
||||
}
|
||||
|
||||
export type RTCNotificationType = "ring" | "notification";
|
||||
|
||||
/**
|
||||
* Represents the intention of the call from the perspective of the sending user.
|
||||
* May be any string, although `"audio"` and `"video"` are commonly accepted values.
|
||||
*/
|
||||
export type RTCCallIntent = "audio" | "video" | string;
|
||||
|
||||
/**
|
||||
* This will check if the content has all the expected fields to be a valid IRTCNotificationContent.
|
||||
* It will also cap the lifetime to 90000ms (1.5 min) if a higher value is provided.
|
||||
* @param content
|
||||
* @throws if the content is invalid
|
||||
* @returns a parsed IRTCNotificationContent
|
||||
*/
|
||||
export function parseCallNotificationContent(content: IContent): IRTCNotificationContent {
|
||||
if (content["m.mentions"] && typeof content["m.mentions"] !== "object") {
|
||||
throw new Error("malformed m.mentions");
|
||||
}
|
||||
if (typeof content["notification_type"] !== "string") {
|
||||
throw new Error("Missing or invalid notification_type");
|
||||
}
|
||||
if (typeof content["sender_ts"] !== "number") {
|
||||
throw new Error("Missing or invalid sender_ts");
|
||||
}
|
||||
if (typeof content["lifetime"] !== "number") {
|
||||
throw new Error("Missing or invalid lifetime");
|
||||
}
|
||||
|
||||
if (content["relation"] && content["relation"]["rel_type"] !== "m.reference") {
|
||||
throw new Error("Invalid relation");
|
||||
}
|
||||
if (content["m.call.intent"] && typeof content["m.call.intent"] !== "string") {
|
||||
throw new Error("Invalid m.call.intent");
|
||||
}
|
||||
|
||||
const cappedLifetime = content["lifetime"] >= 90000 ? 90000 : content["lifetime"];
|
||||
return { ...content, lifetime: cappedLifetime } as IRTCNotificationContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for `org.matrix.msc4075.rtc.notification` events.
|
||||
* Don't cast event content to this directly. Use `parseCallNotificationContent` instead to validate the content first.
|
||||
*/
|
||||
export interface IRTCNotificationContent extends RelationEvent {
|
||||
"m.mentions"?: IMentions;
|
||||
"notification_type": RTCNotificationType;
|
||||
/**
|
||||
* The initial intent of the calling user.
|
||||
*/
|
||||
"m.call.intent"?: RTCCallIntent;
|
||||
"sender_ts": number;
|
||||
"lifetime": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSC4310 decline event content for `org.matrix.msc4310.rtc.decline`.
|
||||
* Sent as a standard m.reference relation to an `org.matrix.msc4075.rtc.notification` event.
|
||||
*/
|
||||
export interface IRTCDeclineContent extends RelationEvent {}
|
||||
|
||||
export enum Status {
|
||||
Disconnected = "Disconnected",
|
||||
Connecting = "Connecting",
|
||||
Connected = "Connected",
|
||||
Disconnecting = "Disconnecting",
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
/**
|
||||
* A type collecting call encryption statistics for a session.
|
||||
*/
|
||||
export type Statistics = {
|
||||
counters: {
|
||||
/**
|
||||
* The number of times we have sent a room event containing encryption keys.
|
||||
*/
|
||||
roomEventEncryptionKeysSent: number;
|
||||
/**
|
||||
* The number of times we have received a room event containing encryption keys.
|
||||
*/
|
||||
roomEventEncryptionKeysReceived: number;
|
||||
};
|
||||
totals: {
|
||||
/**
|
||||
* The total age (in milliseconds) of all room events containing encryption keys that we have received.
|
||||
* We track the total age so that we can later calculate the average age of all keys received.
|
||||
*/
|
||||
roomEventEncryptionKeysReceivedTotalAge: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const isMyMembership = (m: CallMembership, userId: string, deviceId: string): boolean =>
|
||||
m.sender === userId && m.deviceId === deviceId;
|
||||
|
||||
/**
|
||||
* A RTC transport is a JSON object that describes how to connect to a RTC member.
|
||||
*/
|
||||
export interface Transport {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event content for `org.matrix.msc4143.rtc.slot` state events.
|
||||
*/
|
||||
export interface RtcSlotEventContent<T extends string = string> {
|
||||
status: "open" | "closed";
|
||||
application?: RtcSlotApplicationContent<T>;
|
||||
encryption?: RtcSlotEncryptionContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content of the `application` object within `org.matrix.msc4143.rtc.slot` events.
|
||||
*/
|
||||
export interface RtcSlotApplicationContent<T extends string = string> {
|
||||
type: T;
|
||||
// Other application specific keys.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const RTC_SLOT_ENCRYPTION_PER_MEMBER = "org.matrix.msc4143.per_member";
|
||||
|
||||
/**
|
||||
* Content of the `encryption` object within `org.matrix.msc4143.rtc.slot` events.
|
||||
*/
|
||||
export interface RtcSlotEncryptionContent {
|
||||
type: typeof RTC_SLOT_ENCRYPTION_PER_MEMBER;
|
||||
// Other encryption-mechanism specific keys.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* The session description is used to identify a session. Used in the state event.
|
||||
*/
|
||||
export interface SlotDescription {
|
||||
/**
|
||||
* The application type. e.g. "m.call".
|
||||
*/
|
||||
application: string;
|
||||
/**
|
||||
* The application-specific slot ID. e.g. "ROOM".
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
69
node_modules/matrix-js-sdk/src/matrixrtc/utils.ts
generated
vendored
69
node_modules/matrix-js-sdk/src/matrixrtc/utils.ts
generated
vendored
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
Copyright 2025-2026 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 { getEncryptionKeyMapKey, type CallMembershipIdentityParts } from "./EncryptionManager.ts";
|
||||
import type { InboundEncryptionSession, EncryptionKeyMapKey, SlotDescription } from "./types.ts";
|
||||
|
||||
/**
|
||||
* Detects when a key for a given index is outdated.
|
||||
*/
|
||||
export class OutdatedKeyFilter {
|
||||
// Map of participantId -> keyIndex -> timestamp
|
||||
private tsBuffer: Map<EncryptionKeyMapKey, Map<number, number>> = new Map();
|
||||
|
||||
/**
|
||||
* Check if there is a recent key with the same keyId (index) and then use the creationTS to decide what to
|
||||
* do with the key. If the key received is older than the one already in the buffer, it is ignored.
|
||||
* @param participantId
|
||||
* @param item
|
||||
*/
|
||||
public isOutdated(membership: CallMembershipIdentityParts, item: InboundEncryptionSession): boolean {
|
||||
const mapKey = getEncryptionKeyMapKey(membership);
|
||||
if (!this.tsBuffer.has(mapKey)) {
|
||||
this.tsBuffer.set(mapKey, new Map<number, number>());
|
||||
}
|
||||
|
||||
const latestTimestamp = this.tsBuffer.get(mapKey)?.get(item.keyIndex);
|
||||
if (latestTimestamp && latestTimestamp > item.creationTS) {
|
||||
// The existing key is more recent, ignore this one
|
||||
return true;
|
||||
}
|
||||
this.tsBuffer.get(mapKey)!.set(item.keyIndex, item.creationTS);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a slot ID into it's component application and ID portions.
|
||||
* @param slotId e.g. `m.call#call_id`
|
||||
* @throws If the format of `slotId` is invalid.
|
||||
*/
|
||||
export function slotIdToDescription(slotId: string): SlotDescription {
|
||||
const [application, id, ...unexpectedAdditionalValues] = slotId.split("#");
|
||||
if (unexpectedAdditionalValues.length) {
|
||||
throw Error(
|
||||
"MatrixRTC Slot IDs *must* only contain two components seperated by one '#'. Additional '#' characters detected.",
|
||||
);
|
||||
}
|
||||
return { application, id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a SlotDescription into it's slot ID format.
|
||||
*/
|
||||
export function computeSlotId(slotDescription: SlotDescription): string {
|
||||
return `${slotDescription.application}#${slotDescription.id}`;
|
||||
}
|
||||
40
node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts
generated
vendored
40
node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts
generated
vendored
@@ -14,28 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MatrixClient } from "../client.ts";
|
||||
import { RelationType, UNSTABLE_MSC3089_BRANCH } from "../@types/event.ts";
|
||||
import { type IContent, type MatrixEvent } from "./event.ts";
|
||||
import { type MSC3089TreeSpace } from "./MSC3089TreeSpace.ts";
|
||||
import { EventTimeline } from "./event-timeline.ts";
|
||||
import { type FileType } from "../http-api/index.ts";
|
||||
import type { ISendEventResponse } from "../@types/requests.ts";
|
||||
import { type EncryptedFile } from "../@types/media.ts";
|
||||
|
||||
export interface MSC3089EventContent {
|
||||
active?: boolean;
|
||||
name?: string;
|
||||
locked?: boolean;
|
||||
version?: number;
|
||||
}
|
||||
|
||||
export interface MSC3089EventContent {
|
||||
active?: boolean;
|
||||
name?: string;
|
||||
locked?: boolean;
|
||||
version?: number;
|
||||
}
|
||||
import { MatrixClient } from "../client";
|
||||
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../@types/event";
|
||||
import { IContent, MatrixEvent } from "./event";
|
||||
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
import { FileType } from "../http-api";
|
||||
import type { ISendEventResponse } from "../@types/requests";
|
||||
|
||||
/**
|
||||
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
|
||||
@@ -146,7 +131,7 @@ export class MSC3089Branch {
|
||||
* Gets information about the file needed to download it.
|
||||
* @returns Information about the file.
|
||||
*/
|
||||
public async getFileInfo(): Promise<{ info: EncryptedFile; httpUrl: string }> {
|
||||
public async getFileInfo(): Promise<{ info: IEncryptedFile; httpUrl: string }> {
|
||||
const event = await this.getFileEvent();
|
||||
|
||||
const file = event.getOriginalContent()["file"];
|
||||
@@ -177,8 +162,9 @@ export class MSC3089Branch {
|
||||
|
||||
if (!event) throw new Error("Failed to find event");
|
||||
|
||||
// Sometimes the event isn't decrypted for us, so do that.
|
||||
await this.client.decryptEventIfNeeded(event);
|
||||
// Sometimes the event isn't decrypted for us, so do that. We specifically set `emit: true`
|
||||
// to ensure that the relations system in the sdk will function.
|
||||
await this.client.decryptEventIfNeeded(event, { emit: true, isRetry: true });
|
||||
|
||||
return event;
|
||||
}
|
||||
@@ -194,11 +180,11 @@ export class MSC3089Branch {
|
||||
public async createNewVersion(
|
||||
name: string,
|
||||
encryptedContents: FileType,
|
||||
info: EncryptedFile,
|
||||
info: Partial<IEncryptedFile>,
|
||||
additionalContent?: IContent,
|
||||
): Promise<ISendEventResponse> {
|
||||
const fileEventResponse = await this.directory.createFile(name, encryptedContents, info, {
|
||||
...additionalContent,
|
||||
...(additionalContent ?? {}),
|
||||
"m.new_content": true,
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Replace,
|
||||
|
||||
95
node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts
generated
vendored
95
node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts
generated
vendored
@@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MatrixClient } from "../client.ts";
|
||||
import { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event.ts";
|
||||
import { type Room } from "./room.ts";
|
||||
import { logger } from "../logger.ts";
|
||||
import { type IContent, type MatrixEvent } from "./event.ts";
|
||||
import promiseRetry from "p-retry";
|
||||
|
||||
import { MatrixClient } from "../client";
|
||||
import { EventType, IEncryptedFile, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event";
|
||||
import { Room } from "./room";
|
||||
import { logger } from "../logger";
|
||||
import { IContent, MatrixEvent } from "./event";
|
||||
import {
|
||||
averageBetweenStrings,
|
||||
DEFAULT_ALPHABET,
|
||||
@@ -26,14 +28,11 @@ import {
|
||||
nextString,
|
||||
prevString,
|
||||
simpleRetryOperation,
|
||||
} from "../utils.ts";
|
||||
import { MSC3089Branch } from "./MSC3089Branch.ts";
|
||||
import { type ISendEventResponse } from "../@types/requests.ts";
|
||||
import { type FileType, MatrixError } from "../http-api/index.ts";
|
||||
import { KnownMembership } from "../@types/membership.ts";
|
||||
import { type RoomPowerLevelsEventContent, type SpaceChildEventContent } from "../@types/state_events.ts";
|
||||
import type { EncryptedFile, FileContent } from "../@types/media.ts";
|
||||
import { type EmptyObject } from "../@types/common.ts";
|
||||
} from "../utils";
|
||||
import { MSC3089Branch } from "./MSC3089Branch";
|
||||
import { isRoomSharedHistory } from "../crypto/algorithms/megolm";
|
||||
import { ISendEventResponse } from "../@types/requests";
|
||||
import { FileType } from "../http-api";
|
||||
|
||||
/**
|
||||
* The recommended defaults for a tree space's power levels. Note that this
|
||||
@@ -78,12 +77,6 @@ export enum TreePermissions {
|
||||
Owner = "owner", // "Admin" or PL100
|
||||
}
|
||||
|
||||
declare module "../@types/media" {
|
||||
interface FileContent {
|
||||
[UNSTABLE_MSC3089_LEAF.name]?: EmptyObject;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089)
|
||||
* file tree Space. Note that this is UNSTABLE and subject to breaking changes
|
||||
@@ -92,10 +85,7 @@ declare module "../@types/media" {
|
||||
export class MSC3089TreeSpace {
|
||||
public readonly room: Room;
|
||||
|
||||
public constructor(
|
||||
private client: MatrixClient,
|
||||
public readonly roomId: string,
|
||||
) {
|
||||
public constructor(private client: MatrixClient, public readonly roomId: string) {
|
||||
this.room = this.client.getRoom(this.roomId)!;
|
||||
|
||||
if (!this.room) throw new Error("Unknown room");
|
||||
@@ -134,27 +124,40 @@ export class MSC3089TreeSpace {
|
||||
* @param userId - The user ID to invite.
|
||||
* @param andSubspaces - True (default) to invite the user to all
|
||||
* directories/subspaces too, recursively.
|
||||
* @param shareHistoryKeys - True (default) to share encryption keys
|
||||
* with the invited user. This will allow them to decrypt the events (files)
|
||||
* in the tree. Keys will not be shared if the room is lacking appropriate
|
||||
* history visibility (by default, history visibility is "shared" in trees,
|
||||
* which is an appropriate visibility for these purposes).
|
||||
* @returns Promise which resolves when complete.
|
||||
*/
|
||||
public async invite(userId: string, andSubspaces = true): Promise<void> {
|
||||
public async invite(userId: string, andSubspaces = true, shareHistoryKeys = true): Promise<void> {
|
||||
const promises: Promise<void>[] = [this.retryInvite(userId)];
|
||||
if (andSubspaces) {
|
||||
promises.push(...this.getDirectories().map((d) => d.invite(userId, andSubspaces)));
|
||||
promises.push(...this.getDirectories().map((d) => d.invite(userId, andSubspaces, shareHistoryKeys)));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
return Promise.all(promises).then(() => {
|
||||
// Note: key sharing is default on because for file trees it is relatively important that the invite
|
||||
// target can actually decrypt the files. The implied use case is that by inviting a user to the tree
|
||||
// it means the sender would like the receiver to view/download the files contained within, much like
|
||||
// sharing a folder in other circles.
|
||||
if (shareHistoryKeys && isRoomSharedHistory(this.room)) {
|
||||
// noinspection JSIgnoredPromiseFromCall - we aren't concerned as much if this fails.
|
||||
this.client.sendSharedHistoryKeys(this.roomId, [userId]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async retryInvite(userId: string): Promise<void> {
|
||||
await simpleRetryOperation(
|
||||
() => this.client.invite(this.roomId, userId),
|
||||
(e) => {
|
||||
private retryInvite(userId: string): Promise<void> {
|
||||
return simpleRetryOperation(async () => {
|
||||
await this.client.invite(this.roomId, userId).catch((e) => {
|
||||
// We don't want to retry permission errors forever...
|
||||
if (e instanceof MatrixError && e.errcode === "M_FORBIDDEN") {
|
||||
return false;
|
||||
if (e?.errcode === "M_FORBIDDEN") {
|
||||
throw new promiseRetry.AbortError(e);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,7 +172,7 @@ export class MSC3089TreeSpace {
|
||||
const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
|
||||
|
||||
const pls = currentPls?.getContent<RoomPowerLevelsEventContent>() || {};
|
||||
const pls = currentPls?.getContent() || {};
|
||||
const viewLevel = pls["users_default"] || 0;
|
||||
const editLevel = pls["events_default"] || 50;
|
||||
const adminLevel = pls["events"]?.[EventType.RoomPowerLevels] || 100;
|
||||
@@ -227,7 +230,7 @@ export class MSC3089TreeSpace {
|
||||
this.roomId,
|
||||
EventType.SpaceChild,
|
||||
{
|
||||
via: [this.client.getDomain()!],
|
||||
via: [this.client.getDomain()],
|
||||
},
|
||||
directory.roomId,
|
||||
);
|
||||
@@ -236,7 +239,7 @@ export class MSC3089TreeSpace {
|
||||
directory.roomId,
|
||||
EventType.SpaceParent,
|
||||
{
|
||||
via: [this.client.getDomain()!],
|
||||
via: [this.client.getDomain()],
|
||||
},
|
||||
this.roomId,
|
||||
);
|
||||
@@ -285,11 +288,11 @@ export class MSC3089TreeSpace {
|
||||
await dir.delete();
|
||||
}
|
||||
|
||||
const kickMemberships = [KnownMembership.Invite, KnownMembership.Knock, KnownMembership.Join];
|
||||
const kickMemberships = ["invite", "knock", "join"];
|
||||
const members = this.room.currentState.getStateEvents(EventType.RoomMember);
|
||||
for (const member of members) {
|
||||
const isNotUs = member.getStateKey() !== this.client.getUserId();
|
||||
if (isNotUs && kickMemberships.includes(member.getContent().membership! as KnownMembership)) {
|
||||
if (isNotUs && kickMemberships.includes(member.getContent().membership!)) {
|
||||
const stateKey = member.getStateKey();
|
||||
if (!stateKey) {
|
||||
throw new Error("State key not found for branch");
|
||||
@@ -443,9 +446,7 @@ export class MSC3089TreeSpace {
|
||||
// XXX: We should be creating gaps to avoid conflicts
|
||||
lastOrder = lastOrder ? nextString(lastOrder) : DEFAULT_ALPHABET[0];
|
||||
const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, target.roomId);
|
||||
const content = currentChild?.getContent<SpaceChildEventContent>() ?? {
|
||||
via: [this.client.getDomain()!],
|
||||
};
|
||||
const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] };
|
||||
await this.client.sendStateEvent(
|
||||
parentRoom.roomId,
|
||||
EventType.SpaceChild,
|
||||
@@ -468,7 +469,7 @@ export class MSC3089TreeSpace {
|
||||
|
||||
// Now we can finally update our own order state
|
||||
const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, this.roomId);
|
||||
const content = currentChild?.getContent<SpaceChildEventContent>() ?? { via: [this.client.getDomain()!] };
|
||||
const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] };
|
||||
await this.client.sendStateEvent(
|
||||
parentRoom.roomId,
|
||||
EventType.SpaceChild,
|
||||
@@ -494,7 +495,7 @@ export class MSC3089TreeSpace {
|
||||
public async createFile(
|
||||
name: string,
|
||||
encryptedContents: FileType,
|
||||
info: EncryptedFile,
|
||||
info: Partial<IEncryptedFile>,
|
||||
additionalContent?: IContent,
|
||||
): Promise<ISendEventResponse> {
|
||||
const { content_uri: mxc } = await this.client.uploadContent(encryptedContents, {
|
||||
@@ -502,7 +503,7 @@ export class MSC3089TreeSpace {
|
||||
});
|
||||
info.url = mxc;
|
||||
|
||||
const fileContent: FileContent = {
|
||||
const fileContent = {
|
||||
msgtype: MsgType.File,
|
||||
body: name,
|
||||
url: mxc,
|
||||
@@ -521,7 +522,7 @@ export class MSC3089TreeSpace {
|
||||
...additionalContent,
|
||||
...fileContent,
|
||||
[UNSTABLE_MSC3089_LEAF.name]: {},
|
||||
} as FileContent);
|
||||
});
|
||||
|
||||
await this.client.sendStateEvent(
|
||||
this.roomId,
|
||||
|
||||
16
node_modules/matrix-js-sdk/src/models/beacon.ts
generated
vendored
16
node_modules/matrix-js-sdk/src/models/beacon.ts
generated
vendored
@@ -14,16 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MBeaconEventContent } from "../@types/beacon.ts";
|
||||
import {
|
||||
type BeaconInfoState,
|
||||
type BeaconLocationState,
|
||||
parseBeaconContent,
|
||||
parseBeaconInfoContent,
|
||||
} from "../content-helpers.ts";
|
||||
import { type MatrixEvent } from "./event.ts";
|
||||
import { sortEventsByLatestContentTimestamp } from "../utils.ts";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter.ts";
|
||||
import { MBeaconEventContent } from "../@types/beacon";
|
||||
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { sortEventsByLatestContentTimestamp } from "../utils";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
export enum BeaconEvent {
|
||||
New = "Beacon.new",
|
||||
@@ -38,6 +33,7 @@ export type BeaconEventHandlerMap = {
|
||||
[BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
|
||||
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
|
||||
[BeaconEvent.LocationUpdate]: (locationState: BeaconLocationState) => void;
|
||||
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
|
||||
};
|
||||
|
||||
export const isTimestampInDuration = (startTimestamp: number, durationMs: number, timestamp: number): boolean =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user