This commit is contained in:
unfunny
2026-09-12 23:57:45 -04:00
parent 986c0af682
commit dfe4cc537f
1775 changed files with 246421 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
/*
* 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;
}

View File

@@ -0,0 +1,24 @@
/*
Copyright 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.
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 interface IIdentityServerProvider {
/**
* Gets an access token for use against the identity server,
* for the associated client.
* @returns Promise which resolves to the access token.
*/
getAccessToken(): Promise<string | null>;
}

203
node_modules/matrix-js-sdk/src/@types/PushRules.ts generated vendored Normal file
View File

@@ -0,0 +1,203 @@
/*
Copyright 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum PushRuleActionName {
DontNotify = "dont_notify",
Notify = "notify",
Coalesce = "coalesce",
}
export enum TweakName {
Highlight = "highlight",
Sound = "sound",
}
export type Tweak<N extends TweakName, V> = {
set_tweak: N;
value?: V;
};
export type TweakHighlight = Tweak<TweakName.Highlight, boolean>;
export type TweakSound = Tweak<TweakName.Sound, string>;
export type Tweaks = TweakHighlight | TweakSound;
export enum ConditionOperator {
ExactEquals = "==",
LessThan = "<",
GreaterThan = ">",
GreaterThanOrEqual = ">=",
LessThanOrEqual = "<=",
}
export type PushRuleAction = Tweaks | PushRuleActionName;
export type MemberCountCondition<N extends number, Op extends ConditionOperator = ConditionOperator.ExactEquals> =
| `${Op}${N}`
| (Op extends ConditionOperator.ExactEquals ? `${N}` : never);
export type AnyMemberCountCondition = MemberCountCondition<number, ConditionOperator>;
export const DMMemberCountCondition: MemberCountCondition<2> = "2";
export function isDmMemberCountCondition(condition: AnyMemberCountCondition): boolean {
return condition === "==2" || condition === "2";
}
export enum ConditionKind {
EventMatch = "event_match",
EventPropertyIs = "event_property_is",
EventPropertyContains = "event_property_contains",
ContainsDisplayName = "contains_display_name",
RoomMemberCount = "room_member_count",
SenderNotificationPermission = "sender_notification_permission",
CallStarted = "call_started",
CallStartedPrefix = "org.matrix.msc3914.call_started",
}
export interface IPushRuleCondition<N extends ConditionKind | string> {
[k: string]: any; // for custom conditions, there can be other fields here
kind: N;
}
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
key: string;
pattern?: string;
// Note that value property is an optimization for patterns which do not do
// any globbing and when the key is not "content.body".
value?: string;
}
export interface IEventPropertyIsCondition extends IPushRuleCondition<ConditionKind.EventPropertyIs> {
key: string;
value: string | boolean | null | number;
}
export interface IEventPropertyContainsCondition extends IPushRuleCondition<ConditionKind.EventPropertyContains> {
key: string;
value: string | boolean | null | number;
}
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
// no additional fields
}
export interface IRoomMemberCountCondition extends IPushRuleCondition<ConditionKind.RoomMemberCount> {
is: AnyMemberCountCondition;
}
export interface ISenderNotificationPermissionCondition extends IPushRuleCondition<ConditionKind.SenderNotificationPermission> {
key: string;
}
export interface ICallStartedCondition extends IPushRuleCondition<ConditionKind.CallStarted> {
// no additional fields
}
export interface ICallStartedPrefixCondition extends IPushRuleCondition<ConditionKind.CallStartedPrefix> {
// no additional fields
}
// XXX: custom conditions are possible but always fail, and break the typescript discriminated union so ignore them here
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
export type PushRuleCondition =
| IEventMatchCondition
| IEventPropertyIsCondition
| IEventPropertyContainsCondition
| IContainsDisplayNameCondition
| IRoomMemberCountCondition
| ISenderNotificationPermissionCondition
| ICallStartedCondition
| ICallStartedPrefixCondition;
export enum PushRuleKind {
Override = "override",
ContentSpecific = "content",
RoomSpecific = "room",
SenderSpecific = "sender",
Underride = "underride",
}
export enum RuleId {
Master = ".m.rule.master",
IsUserMention = ".m.rule.is_user_mention",
IsRoomMention = ".m.rule.is_room_mention",
ContainsDisplayName = ".m.rule.contains_display_name",
ContainsUserName = ".m.rule.contains_user_name",
AtRoomNotification = ".m.rule.roomnotif",
DM = ".m.rule.room_one_to_one",
EncryptedDM = ".m.rule.encrypted_room_one_to_one",
Message = ".m.rule.message",
EncryptedMessage = ".m.rule.encrypted",
InviteToSelf = ".m.rule.invite_for_me",
MemberEvent = ".m.rule.member_event",
IncomingCall = ".m.rule.call",
SuppressNotices = ".m.rule.suppress_notices",
Tombstone = ".m.rule.tombstone",
PollStart = ".m.rule.poll_start",
PollStartUnstable = ".org.matrix.msc3930.rule.poll_start",
PollEnd = ".m.rule.poll_end",
PollEndUnstable = ".org.matrix.msc3930.rule.poll_end",
PollStartOneToOne = ".m.rule.poll_start_one_to_one",
PollStartOneToOneUnstable = ".org.matrix.msc3930.rule.poll_start_one_to_one",
PollEndOneToOne = ".m.rule.poll_end_one_to_one",
PollEndOneToOneUnstable = ".org.matrix.msc3930.rule.poll_end_one_to_one",
}
export type PushRuleSet = {
[k in PushRuleKind]?: IPushRule[];
};
export interface IPushRule {
actions: PushRuleAction[];
conditions?: PushRuleCondition[];
default: boolean;
enabled: boolean;
pattern?: string;
rule_id: RuleId | string;
}
export interface IAnnotatedPushRule extends IPushRule {
kind: PushRuleKind;
}
export interface IPushRules {
global: PushRuleSet;
device?: PushRuleSet;
}
export interface IPusher {
"app_display_name": string;
"app_id": string;
"data": {
format?: string;
url?: string; // TODO: Required if kind==http
brand?: string; // TODO: For email notifications only? Unspecced field
};
"device_display_name": string;
"kind": "http" | string;
"lang": string;
"profile_tag"?: string;
"pushkey": string;
"enabled"?: boolean | null;
"org.matrix.msc3881.enabled"?: boolean | null;
"device_id"?: string | null;
"org.matrix.msc3881.device_id"?: string | null;
}
export interface IPusherRequest extends Omit<IPusher, "device_id" | "org.matrix.msc3881.device_id"> {
append?: boolean;
}

View File

@@ -0,0 +1,19 @@
/*
Copyright 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.
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.
*/
declare module "another-json" {
export function stringify(o: object): string;
}

246
node_modules/matrix-js-sdk/src/@types/auth.ts generated vendored Normal file
View File

@@ -0,0 +1,246 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { UnstableValue } from "../NamespacedValue.ts";
import { type IClientWellKnown } from "../client.ts";
/**
* Represents a response to the CSAPI `/refresh` endpoint.
*/
export interface IRefreshTokenResponse {
access_token: string;
expires_in_ms: number;
refresh_token: string;
}
/**
* Response to GET login flows as per https://spec.matrix.org/v1.3/client-server-api/#get_matrixclientv3login
*/
export interface ILoginFlowsResponse {
flows: LoginFlow[];
}
export type LoginFlow = ISSOFlow | IPasswordFlow | ILoginFlow;
export interface ILoginFlow {
type: string;
}
export interface IPasswordFlow extends ILoginFlow {
type: "m.login.password";
}
export const OAUTH_AWARE_PREFERRED_FLOW_FIELD = new UnstableValue(
"oauth_aware_preferred",
"org.matrix.msc3824.delegated_oidc_compatibility",
);
/**
* Representation of SSO flow as per https://spec.matrix.org/v1.3/client-server-api/#client-login-via-sso
*/
export interface ISSOFlow extends ILoginFlow {
type: "m.login.sso" | "m.login.cas";
identity_providers?: IIdentityProvider[];
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.name]?: boolean;
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.altName]?: boolean;
}
export enum IdentityProviderBrand {
Gitlab = "gitlab",
Github = "github",
Apple = "apple",
Google = "google",
Facebook = "facebook",
Twitter = "twitter",
}
export interface IIdentityProvider {
id: string;
name: string;
icon?: string;
brand?: IdentityProviderBrand | string;
}
export enum SSOAction {
/** The user intends to login to an existing account */
LOGIN = "login",
/** The user intends to register for a new account */
REGISTER = "register",
}
/**
* A client can identify a user using their Matrix ID.
* This can either be the fully qualified Matrix user ID, or just the localpart of the user ID.
* @see https://spec.matrix.org/v1.7/client-server-api/#matrix-user-id
*/
type UserLoginIdentifier = {
type: "m.id.user";
user: string;
};
/**
* A client can identify a user using a 3PID associated with the user’s account on the homeserver,
* where the 3PID was previously associated using the /account/3pid API.
* See the 3PID Types Appendix for a list of Third-party ID media.
* @see https://spec.matrix.org/v1.7/client-server-api/#third-party-id
*/
type ThirdPartyLoginIdentifier = {
type: "m.id.thirdparty";
medium: string;
address: string;
};
/**
* A client can identify a user using a phone number associated with the user’s account,
* where the phone number was previously associated using the /account/3pid API.
* The phone number can be passed in as entered by the user; the homeserver will be responsible for canonicalising it.
* If the client wishes to canonicalise the phone number,
* then it can use the m.id.thirdparty identifier type with a medium of msisdn instead.
*
* The country is the two-letter uppercase ISO-3166-1 alpha-2 country code that the number in phone should be parsed as if it were dialled from.
*
* @see https://spec.matrix.org/v1.7/client-server-api/#phone-number
*/
type PhoneLoginIdentifier = {
type: "m.id.phone";
country: string;
phone: string;
};
type SpecUserIdentifier = UserLoginIdentifier | ThirdPartyLoginIdentifier | PhoneLoginIdentifier;
/**
* User Identifiers usable for login & user-interactive authentication.
*
* Extensibly allows more than Matrix specified identifiers.
*/
export type UserIdentifier =
| SpecUserIdentifier
| { type: Exclude<string, SpecUserIdentifier["type"]>; [key: string]: any };
/**
* Request body for POST /login request
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3login
*/
export interface LoginRequest {
/**
* The login type being used.
*/
type: "m.login.password" | "m.login.token" | string;
/**
* ID of the client device.
* If this does not correspond to a known client device, a new device will be created.
* The given device ID must not be the same as a cross-signing key ID.
* The server will auto-generate a device_id if this is not specified.
*/
device_id?: string;
/**
* Identification information for a user
*/
identifier?: UserIdentifier;
/**
* A display name to assign to the newly-created device.
* Ignored if device_id corresponds to a known device.
*/
initial_device_display_name?: string;
/**
* When logging in using a third-party identifier, the medium of the identifier.
* Must be `email`.
* @deprecated in favour of `identifier`.
*/
medium?: "email";
/**
* Required when type is `m.login.password`. The user’s password.
*/
password?: string;
/**
* If true, the client supports refresh tokens.
*/
refresh_token?: boolean;
/**
* Required when type is `m.login.token`. Part of Token-based login.
*/
token?: string;
/**
* The fully qualified user ID or just local part of the user ID, to log in.
* @deprecated in favour of identifier.
*/
user?: string;
// Extensible
[key: string]: any;
}
// Export for backwards compatibility
export type ILoginParams = LoginRequest;
/**
* Response body for POST /login request
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3login
*/
export interface LoginResponse {
/**
* An access token for the account.
* This access token can then be used to authorize other requests.
*/
access_token: string;
/**
* ID of the logged-in device.
* Will be the same as the corresponding parameter in the request, if one was specified.
*/
device_id: string;
/**
* The fully-qualified Matrix ID for the account.
*/
user_id: string;
/**
* The lifetime of the access token, in milliseconds.
* Once the access token has expired a new access token can be obtained by using the provided refresh token.
* If no refresh token is provided, the client will need to re-log in to obtain a new access token.
* If not given, the client can assume that the access token will not expire.
*/
expires_in_ms?: number;
/**
* A refresh token for the account.
* This token can be used to obtain a new access token when it expires by calling the /refresh endpoint.
*/
refresh_token?: string;
/**
* Optional client configuration provided by the server.
* If present, clients SHOULD use the provided object to reconfigure themselves, optionally validating the URLs within.
* This object takes the same form as the one returned from .well-known autodiscovery.
*/
well_known?: IClientWellKnown;
/**
* 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;
}
/**
* The result of a successful `m.login.token` issuance request as per https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv1loginget_token
*/
export interface LoginTokenPostResponse {
/**
* The token to use with `m.login.token` to authenticate.
*/
login_token: string;
/**
* Expiration in milliseconds.
*/
expires_in_ms: number;
}

140
node_modules/matrix-js-sdk/src/@types/beacon.ts generated vendored Normal file
View File

@@ -0,0 +1,140 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type RelatesToRelationship, type REFERENCE_RELATION } from "./extensible_events.ts";
import { UnstableValue } from "../NamespacedValue.ts";
import { type MAssetEvent, type MLocationEvent, type MTimestampEvent } from "./location.ts";
/**
* Beacon info and beacon event types as described in MSC3672
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
*/
/**
* Beacon info events are state events.
* We have two requirements for these events:
* 1. they can only be written by their owner
* 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`.
* To achieve an arbitrary number of only owner-writable state events
* we introduce a variable suffix to the event type
*
* @example
* ```
* {
* "type": "m.beacon_info.@matthew:matrix.org.1",
* "state_key": "@matthew:matrix.org",
* "content": {
* "m.beacon_info": {
* "description": "The Matthew Tracker",
* "timeout": 86400000,
* },
* // more content as described below
* }
* },
* {
* "type": "m.beacon_info.@matthew:matrix.org.2",
* "state_key": "@matthew:matrix.org",
* "content": {
* "m.beacon_info": {
* "description": "Another different Matthew tracker",
* "timeout": 400000,
* },
* // more content as described below
* }
* }
* ```
*/
/**
* Non-variable type for m.beacon_info event content
*/
export const M_BEACON_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3672.beacon_info");
export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3672.beacon");
export type MBeaconInfoContent = {
description?: string;
// how long from the last event until we consider the beacon inactive in milliseconds
timeout: number;
// true when this is a live location beacon
// https://github.com/matrix-org/matrix-spec-proposals/pull/3672
live?: boolean;
};
/**
* m.beacon_info Event example from the spec
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
* @example
* ```
* {
* "type": "m.beacon_info",
* "state_key": "@matthew:matrix.org",
* "content": {
* "m.beacon_info": {
* "description": "The Matthew Tracker", // same as an `m.location` description
* "timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds
* },
* "m.ts": 1436829458432, // creation timestamp of the beacon on the client
* "m.asset": {
* "type": "m.self" // the type of asset being tracked as per MSC3488
* }
* }
* }
* ```
*/
/**
* m.beacon_info.* event content
*/
export type MBeaconInfoEventContent = MBeaconInfoContent &
// creation timestamp of the beacon on the client
MTimestampEvent &
// the type of asset being tracked as per MSC3488
MAssetEvent;
/**
* m.beacon event example
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
* @example
* ```
* {
* "type": "m.beacon",
* "sender": "@matthew:matrix.org",
* "content": {
* "m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674
* "rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267
* "event_id": "$beacon_info"
* },
* "m.location": {
* "uri": "geo:51.5008,0.1247;u=35",
* "description": "Arbitrary beacon information"
* },
* "m.ts": 1636829458432,
* }
* }
* ```
*/
/**
* Content of an m.beacon event
*/
export type MBeaconEventContent = MLocationEvent &
// timestamp when location was taken
MTimestampEvent &
// relates to a beacon_info event
RelatesToRelationship<typeof REFERENCE_RELATION>;

24
node_modules/matrix-js-sdk/src/@types/common.ts generated vendored Normal file
View File

@@ -0,0 +1,24 @@
/*
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>;

67
node_modules/matrix-js-sdk/src/@types/crypto.ts generated vendored Normal file
View File

@@ -0,0 +1,67 @@
/*
Copyright 2022-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.
*/
import type { ISignatures } from "./signed.ts";
import type { EventDecryptionResult } from "../common-crypto/CryptoBackend.ts";
// Backwards compatible re-export
/** @deprecated This is an internal type and should not be used. */
type IEventDecryptionResult = EventDecryptionResult;
export type { IEventDecryptionResult };
interface Extensible {
[key: string]: any;
}
/** The result of a call to {@link crypto-api!CryptoApi.exportRoomKeys} */
export interface IMegolmSessionData extends Extensible {
/** Sender's Curve25519 device key */
sender_key: string;
/** Devices which forwarded this session to us (normally empty). */
forwarding_curve25519_key_chain: string[];
/** Other keys the sender claims. */
sender_claimed_keys: Record<string, string>;
/** Room this session is used in */
room_id: string;
/** Unique id for the session */
session_id: string;
/** Base64'ed key data */
session_key: string;
algorithm?: string;
untrusted?: boolean;
}
/** 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;
keys: Record<string, string>;
signatures?: ISignatures;
}
/** the type of the `one_time_keys` and `fallback_keys` parameters on `/_matrix/client/v3/keys/upload`
*
* @see https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3keysupload
*/
export interface IOneTimeKey {
key: string;
fallback?: boolean;
signatures?: ISignatures;
}

473
node_modules/matrix-js-sdk/src/@types/event.ts generated vendored Normal file
View File

@@ -0,0 +1,473 @@
/*
Copyright 2020-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 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";
export enum EventType {
// Room state events
RoomCanonicalAlias = "m.room.canonical_alias",
RoomCreate = "m.room.create",
RoomJoinRules = "m.room.join_rules",
RoomMember = "m.room.member",
RoomThirdPartyInvite = "m.room.third_party_invite",
RoomPowerLevels = "m.room.power_levels",
RoomName = "m.room.name",
RoomTopic = "m.room.topic",
RoomAvatar = "m.room.avatar",
RoomPinnedEvents = "m.room.pinned_events",
RoomEncryption = "m.room.encryption",
RoomHistoryVisibility = "m.room.history_visibility",
RoomGuestAccess = "m.room.guest_access",
RoomServerAcl = "m.room.server_acl",
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",
// Room timeline events
RoomRedaction = "m.room.redaction",
RoomMessage = "m.room.message",
RoomMessageEncrypted = "m.room.encrypted",
Sticker = "m.sticker",
CallInvite = "m.call.invite",
CallCandidates = "m.call.candidates",
CallAnswer = "m.call.answer",
CallHangup = "m.call.hangup",
CallReject = "m.call.reject",
CallSelectAnswer = "m.call.select_answer",
CallNegotiate = "m.call.negotiate",
CallSDPStreamMetadataChanged = "m.call.sdp_stream_metadata_changed",
CallSDPStreamMetadataChangedPrefix = "org.matrix.call.sdp_stream_metadata_changed",
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",
KeyVerificationMac = "m.key.verification.mac",
KeyVerificationDone = "m.key.verification.done",
KeyVerificationKey = "m.key.verification.key",
KeyVerificationAccept = "m.key.verification.accept",
// Not used directly - see READY_TYPE in VerificationRequest.
KeyVerificationReady = "m.key.verification.ready",
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
RoomMessageFeedback = "m.room.message.feedback",
Reaction = "m.reaction",
PollStart = "org.matrix.msc3381.poll.start",
// Room ephemeral events
Typing = "m.typing",
Receipt = "m.receipt",
Presence = "m.presence",
// Room account_data events
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",
}
export enum MsgType {
Text = "m.text",
Emote = "m.emote",
Notice = "m.notice",
Image = "m.image",
File = "m.file",
Audio = "m.audio",
Location = "m.location",
Video = "m.video",
KeyVerificationRequest = "m.key.verification.request",
}
export const RoomCreateTypeField = "type";
export enum RoomType {
Space = "m.space",
UnstableCall = "org.matrix.msc3417.call",
ElementVideo = "io.element.video",
}
export const ToDeviceMessageId = "org.matrix.msgid";
/**
* Identifier for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
* room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
* including its eventual removal.
*/
export const UNSTABLE_MSC3088_PURPOSE = new UnstableValue("m.room.purpose", "org.matrix.msc3088.purpose");
/**
* Enabled flag for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
* room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
* including its eventual removal.
*/
export const UNSTABLE_MSC3088_ENABLED = new UnstableValue("m.enabled", "org.matrix.msc3088.enabled");
/**
* Subtype for an [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
* Note that this reference is UNSTABLE and subject to breaking changes, including its
* eventual removal.
*/
export const UNSTABLE_MSC3089_TREE_SUBTYPE = new UnstableValue("m.data_tree", "org.matrix.msc3089.data_tree");
/**
* Leaf type for an event in a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
* Note that this reference is UNSTABLE and subject to breaking changes, including its
* eventual removal.
*/
export const UNSTABLE_MSC3089_LEAF = new UnstableValue("m.leaf", "org.matrix.msc3089.leaf");
/**
* Branch (Leaf Reference) type for the index approach in a
* [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room. Note that this reference is
* UNSTABLE and subject to breaking changes, including its eventual removal.
*/
export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix.msc3089.branch");
/**
* Marker event type to point back at imported historical content in a room. See
* [MSC2716](https://github.com/matrix-org/matrix-spec-proposals/pull/2716).
* Note that this reference is UNSTABLE and subject to breaking changes,
* including its eventual removal.
*/
export const UNSTABLE_MSC2716_MARKER = new UnstableValue("m.room.marker", "org.matrix.msc2716.marker");
/**
* Name of the request property for relation based redactions.
* {@link https://github.com/matrix-org/matrix-spec-proposals/pull/3912}
*/
export const MSC3912_RELATION_BASED_REDACTIONS_PROP = new UnstableValue(
"with_rel_types",
"org.matrix.msc3912.with_relations",
);
/**
* Functional members type for declaring a purpose of room members (e.g. helpful bots).
* Note that this reference is UNSTABLE and subject to breaking changes, including its
* eventual removal.
*
* Schema (TypeScript):
* ```
* {
* service_members?: string[]
* }
* ```
*
* @example
* ```
* {
* "service_members": [
* "@helperbot:localhost",
* "@reminderbot:alice.tdl"
* ]
* }
* ```
*/
export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
"io.element.functional_members",
"io.element.functional_members",
);
/**
* A type of message that affects visibility of a message,
* as per https://github.com/matrix-org/matrix-doc/pull/3531
*
* @experimental
*/
export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue("m.visibility", "org.matrix.msc3531.visibility");
/**
* https://github.com/matrix-org/matrix-doc/pull/3881
*
* @experimental
*/
export const PUSHER_ENABLED = new UnstableValue("enabled", "org.matrix.msc3881.enabled");
/**
* https://github.com/matrix-org/matrix-doc/pull/3881
*
* @experimental
*/
export const PUSHER_DEVICE_ID = new UnstableValue("device_id", "org.matrix.msc3881.device_id");
/**
* https://github.com/matrix-org/matrix-doc/pull/3890
*
* @experimental
*/
export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
"m.local_notification_settings",
"org.matrix.msc3890.local_notification_settings",
);
/**
* https://github.com/matrix-org/matrix-doc/pull/4023
*
* @experimental
*/
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;
}>;
};
}
/**
* 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;
}

119
node_modules/matrix-js-sdk/src/@types/events.ts generated vendored Normal file
View File

@@ -0,0 +1,119 @@
/*
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;
};
}

View File

@@ -0,0 +1,146 @@
/*
Copyright 2021 - 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.
*/
import { type EitherAnd, NamespacedValue, UnstableValue } from "matrix-events-sdk";
import { isProvided } from "../extensible_events_v1/utilities.ts";
// 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;
/**
* Represents a namespaced value, if the value is a string. Used to extract provided types
* from a TSNamespace<N> (in cases where only stable *or* unstable is provided).
*/
export type TSNamespaceValue<V> = V extends string ? V : never;
/**
* Creates a type which is V when T is `never`, otherwise T.
*/
// See https://github.com/microsoft/TypeScript/issues/23182#issuecomment-379091887 for details on the array syntax.
export type DefaultNever<T, V> = [T] extends [never] ? V : T;
/**
* The namespaced value for m.message
*/
export const M_MESSAGE = new UnstableValue("m.message", "org.matrix.msc1767.message");
/**
* An m.message event rendering
*/
export interface IMessageRendering {
body: string;
mimetype?: string;
}
/**
* The content for an m.message event
*/
export type ExtensibleMessageEventContent = EitherAnd<
{ [M_MESSAGE.name]: IMessageRendering[] },
{ [M_MESSAGE.altName]: IMessageRendering[] }
>;
/**
* The namespaced value for m.text
*/
export const M_TEXT = new UnstableValue("m.text", "org.matrix.msc1767.text");
/**
* The content for an m.text event
*/
export type TextEventContent = EitherAnd<{ [M_TEXT.name]: string }, { [M_TEXT.altName]: string }>;
/**
* The namespaced value for m.html
*/
export const M_HTML = new UnstableValue("m.html", "org.matrix.msc1767.html");
/**
* The content for an m.html event
*/
export type HtmlEventContent = EitherAnd<{ [M_HTML.name]: string }, { [M_HTML.altName]: string }>;
/**
* The content for an m.message, m.text, or m.html event
*/
export type ExtensibleAnyMessageEventContent = ExtensibleMessageEventContent | TextEventContent | HtmlEventContent;
/**
* The namespaced value for an m.reference relation
*/
export const REFERENCE_RELATION = new NamespacedValue("m.reference");
/**
* Represents any relation type
*/
export type AnyRelation = TSNamespace<typeof REFERENCE_RELATION> | string;
/**
* An m.relates_to relationship
*/
export type RelatesToRelationship<R = 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;
};
};
/**
* Partial types for a Matrix Event.
*/
export interface IPartialEvent<TContent> {
type: string;
content: TContent;
}
/**
* Represents a potentially namespaced event type.
*/
export type ExtensibleEventType = NamespacedValue<string, string> | string;
/**
* Determines if two event types are the same, including namespaces.
* @param given - The given event type. This will be compared
* against the expected type.
* @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 {
if (typeof given === "string") {
if (typeof expected === "string") {
return expected === given;
} else {
return expected!.matches(given);
}
} else {
if (typeof expected === "string") {
return given!.matches(expected);
} else {
const expectedNs = expected!;
const givenNs = given!;
return (
expectedNs.matches(givenNs.name) || (isProvided(givenNs.altName) && expectedNs.matches(givenNs.altName))
);
}
}
}

67
node_modules/matrix-js-sdk/src/@types/global.d.ts generated vendored Normal file
View File

@@ -0,0 +1,67 @@
/*
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.
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 {};
declare global {
// use `number` as the return type in all cases for globalThis.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;
namespace NodeJS {
interface Global {
// 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 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;
}
export interface Uint8ArrayToBase64Options {
alphabet?: "base64" | "base64url";
omitPadding?: boolean;
}
interface Uint8Array {
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
toBase64?(options?: Uint8ArrayToBase64Options): string;
}
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 Uint8ArrayConstructor {
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64
fromBase64?(base64: string, options?: Uint8ArrayFromBase64Options): Uint8Array<ArrayBuffer>;
}
}

16
node_modules/matrix-js-sdk/src/@types/json.ts generated vendored Normal file
View File

@@ -0,0 +1,16 @@
/*
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;

View File

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

92
node_modules/matrix-js-sdk/src/@types/location.ts generated vendored Normal file
View File

@@ -0,0 +1,92 @@
/*
Copyright 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.
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.
*/
// Types for MSC3488 - m.location: Extending events with location data
import { type EitherAnd } from "matrix-events-sdk";
import { UnstableValue } from "../NamespacedValue.ts";
import { type M_TEXT } from "./extensible_events.ts";
export enum LocationAssetType {
Self = "m.self",
Pin = "m.pin",
}
export const M_ASSET = new UnstableValue("m.asset", "org.matrix.msc3488.asset");
export type MAssetContent = { type: LocationAssetType };
/**
* The event definition for an m.asset event (in content)
*/
export type MAssetEvent = EitherAnd<{ [M_ASSET.name]: MAssetContent }, { [M_ASSET.altName]: MAssetContent }>;
export const M_TIMESTAMP = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
/**
* The event definition for an m.ts event (in content)
*/
export type MTimestampEvent = EitherAnd<{ [M_TIMESTAMP.name]: number }, { [M_TIMESTAMP.altName]: number }>;
export const M_LOCATION = new UnstableValue("m.location", "org.matrix.msc3488.location");
export type MLocationContent = {
uri: string;
description?: string | null;
};
export type MLocationEvent = EitherAnd<
{ [M_LOCATION.name]: MLocationContent },
{ [M_LOCATION.altName]: MLocationContent }
>;
export type MTextEvent = EitherAnd<{ [M_TEXT.name]: string }, { [M_TEXT.altName]: string }>;
/* From the spec at:
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
{
"type": "m.room.message",
"content": {
"body": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
"msgtype": "m.location",
"geo_uri": "geo:51.5008,0.1247;u=35",
"m.location": {
"uri": "geo:51.5008,0.1247;u=35",
"description": "Matthew's whereabouts",
},
"m.asset": {
"type": "m.self"
},
"m.text": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
"m.ts": 1636829458432,
}
}
*/
type OptionalTimestampEvent = MTimestampEvent | undefined;
/**
* The content for an m.location event
*/
export type MLocationEventContent = MLocationEvent & MAssetEvent & MTextEvent & OptionalTimestampEvent;
export type LegacyLocationEventContent = {
body: string;
msgtype: string;
geo_uri: string;
};
/**
* Possible content for location events as sent over the wire
*/
export type LocationEventWireContent = Partial<LegacyLocationEventContent & MLocationEventContent>;
export type ILocationContent = MLocationEventContent & LegacyLocationEventContent;

View File

@@ -0,0 +1,38 @@
/*
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 Normal file
View File

@@ -0,0 +1,245 @@
/*
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 Normal file
View File

@@ -0,0 +1,57 @@
/*
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;

101
node_modules/matrix-js-sdk/src/@types/partials.ts generated vendored Normal file
View File

@@ -0,0 +1,101 @@
/*
Copyright 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum Visibility {
Public = "public",
Private = "private",
}
export enum Preset {
PrivateChat = "private_chat",
TrustedPrivateChat = "trusted_private_chat",
PublicChat = "public_chat",
}
export type ResizeMethod = "crop" | "scale";
export type IdServerUnbindResult = "no-support" | "success";
// Knock and private are reserved keywords which are not yet implemented.
export enum JoinRule {
Public = "public",
Invite = "invite",
/**
* @deprecated Reserved keyword. Should not be used. Not yet implemented.
*/
Private = "private",
Knock = "knock",
Restricted = "restricted",
}
export enum RestrictedAllowType {
RoomMembership = "m.room_membership",
}
export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
export enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
WorldReadable = "world_readable",
}
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
limit_type: "monthly_active_user" | "hs_disabled" | string;
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;
};
}

120
node_modules/matrix-js-sdk/src/@types/polls.ts generated vendored Normal file
View File

@@ -0,0 +1,120 @@
/*
Copyright 2022 - 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.
*/
import { type 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";
/**
* Identifier for a disclosed poll.
*/
export const M_POLL_KIND_DISCLOSED = new UnstableValue("m.poll.disclosed", "org.matrix.msc3381.poll.disclosed");
/**
* Identifier for an undisclosed poll.
*/
export const M_POLL_KIND_UNDISCLOSED = new UnstableValue("m.poll.undisclosed", "org.matrix.msc3381.poll.undisclosed");
/**
* Any poll kind.
*/
export type PollKind = TSNamespace<typeof M_POLL_KIND_DISCLOSED> | TSNamespace<typeof M_POLL_KIND_UNDISCLOSED> | string;
/**
* Known poll kind namespaces.
*/
export type KnownPollKind = typeof M_POLL_KIND_DISCLOSED | typeof M_POLL_KIND_UNDISCLOSED;
/**
* The namespaced value for m.poll.start
*/
export const M_POLL_START = new UnstableValue("m.poll.start", "org.matrix.msc3381.poll.start");
/**
* The m.poll.start type within event content
*/
export type PollStartSubtype = {
question: ExtensibleAnyMessageEventContent;
kind: PollKind;
max_selections?: number; // default 1, always positive
answers: PollAnswer[];
};
/**
* A poll answer.
*/
export type PollAnswer = ExtensibleAnyMessageEventContent & { id: string };
/**
* The event definition for an m.poll.start event (in content)
*/
export type PollStartEvent = EitherAnd<
{ [M_POLL_START.name]: PollStartSubtype },
{ [M_POLL_START.altName]: PollStartSubtype }
>;
/**
* The content for an m.poll.start event
*/
export type PollStartEventContent = PollStartEvent & ExtensibleAnyMessageEventContent;
/**
* The namespaced value for m.poll.response
*/
export const M_POLL_RESPONSE = new UnstableValue("m.poll.response", "org.matrix.msc3381.poll.response");
/**
* The m.poll.response type within event content
*/
export type PollResponseSubtype = {
answers: string[];
};
/**
* The event definition for an m.poll.response event (in content)
*/
export type PollResponseEvent = EitherAnd<
{ [M_POLL_RESPONSE.name]: PollResponseSubtype },
{ [M_POLL_RESPONSE.altName]: PollResponseSubtype }
>;
/**
* The content for an m.poll.response event
*/
export type PollResponseEventContent = PollResponseEvent & RelatesToRelationship<typeof REFERENCE_RELATION>;
/**
* The namespaced value for m.poll.end
*/
export const M_POLL_END = new UnstableValue("m.poll.end", "org.matrix.msc3381.poll.end");
/**
* 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 }>;
/**
* The content for an m.poll.end event
*/
export type PollEndEventContent = PollEndEvent &
RelatesToRelationship<typeof REFERENCE_RELATION> &
ExtensibleAnyMessageEventContent;

61
node_modules/matrix-js-sdk/src/@types/read_receipts.ts generated vendored Normal file
View File

@@ -0,0 +1,61 @@
/*
Copyright 2022 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum ReceiptType {
Read = "m.read",
FullyRead = "m.fully_read",
ReadPrivate = "m.read.private",
}
export const MAIN_ROOM_TIMELINE = "main";
export interface Receipt {
ts: number;
thread_id?: string;
}
export interface WrappedReceipt {
eventId: string;
data: Receipt;
}
export interface CachedReceipt {
type: ReceiptType;
userId: string;
data: Receipt;
}
export type ReceiptCache = Map<string, CachedReceipt[]>;
export interface ReceiptContent {
[eventId: string]: {
[key in ReceiptType | string]: {
[userId: string]: Receipt;
};
};
}
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
// map: receipt type → user Id → receipt
export type Receipts = Map<string, Map<string, [real: WrappedReceipt | null, synthetic: WrappedReceipt | null]>>;
export type CachedReceiptStructure = {
eventId: string;
receiptType: string | ReceiptType;
userId: string;
receipt: Receipt;
synthetic: boolean;
};

102
node_modules/matrix-js-sdk/src/@types/registration.ts generated vendored Normal file
View File

@@ -0,0 +1,102 @@
/*
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.
*/
import { type AuthDict } from "../interactive-auth.ts";
/**
* The request body of a call to `POST /_matrix/client/v3/register`.
*
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
*/
export interface RegisterRequest {
/**
* Additional authentication information for the user-interactive authentication API.
* Note that this information is not used to define how the registered user should be authenticated,
* but is instead used to authenticate the register call itself.
*/
auth?: AuthDict;
/**
* The basis for the localpart of the desired Matrix ID.
* If omitted, the homeserver MUST generate a Matrix ID local part.
*/
username?: string;
/**
* The desired password for the account.
*/
password?: string;
/**
* If true, the client supports refresh tokens.
*/
refresh_token?: boolean;
/**
* If true, an access_token and device_id should not be returned from this call, therefore preventing an automatic login.
* Defaults to false.
*/
inhibit_login?: boolean;
/**
* A display name to assign to the newly-created device.
* Ignored if device_id corresponds to a known device.
*/
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
*/
guest_access_token?: string;
}
/**
* The result of a successful call to `POST /_matrix/client/v3/register`.
*
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
*/
export interface RegisterResponse {
/**
* The fully-qualified Matrix user ID (MXID) that has been registered.
*/
user_id: string;
/**
* An access token for the account.
* This access token can then be used to authorize other requests.
* Required if the inhibit_login option is false.
*/
access_token?: string;
/**
* ID of the registered device.
* Will be the same as the corresponding parameter in the request, if one was specified.
* Required if the inhibit_login option is false.
*/
device_id?: string;
/**
* The lifetime of the access token, in milliseconds.
* Once the access token has expired a new access token can be obtained by using the provided refresh token.
* If no refresh token is provided, the client will need to re-log in to obtain a new access token.
* If not given, the client can assume that the access token will not expire.
*
* Omitted if the inhibit_login option is true.
*/
expires_in_ms?: number;
/**
* A refresh token for the account.
* This token can be used to obtain a new access token when it expires by calling the /refresh endpoint.
*
* Omitted if the inhibit_login option is true.
*/
refresh_token?: string;
}

340
node_modules/matrix-js-sdk/src/@types/requests.ts generated vendored Normal file
View File

@@ -0,0 +1,340 @@
/*
Copyright 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.
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, 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";
export interface IJoinRoomOpts {
/**
* If the caller has a keypair 3pid invite, the signing URL is passed in this parameter.
*/
inviteSignUrl?: string;
/**
* 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 {
reason?: string;
/**
* If specified, then any events which relate to the event being redacted with
* any of the relationship types listed will also be redacted.
* Provide a "*" list item to tell the server to redact relations of any type.
*
* <b>Raises an Error if the server does not support it.</b>
* Check for server-side support before using this param with
* <code>client.canSupport.get(Feature.RelationBasedRedactions)</code>.
* {@link https://github.com/matrix-org/matrix-spec-proposals/pull/3912}
*/
with_rel_types?: Array<RelationType | "*">;
}
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";
// The status message to attach.
status_msg?: string;
}
export interface IPaginateOpts {
// true to fill backwards, false to go forwards
backwards?: boolean;
// number of events to request
limit?: number;
}
export interface IGuestAccessOpts {
/**
* True to allow guests to join this room. This
* implicitly gives guests write access. If false or not given, guests are
* explicitly forbidden from joining the room.
*/
allowJoin: boolean;
/**
* True to set history visibility to
* be world_readable. This gives guests read access *from this point forward*.
* If false or not given, history visibility is not modified.
*/
allowRead: boolean;
}
export interface ISearchOpts {
keys?: SearchKey[];
query: string;
}
export interface IEventSearchOpts {
// a JSON filter object to pass in the request
filter?: IRoomEventFilter;
// the term to search for
term: string;
}
export interface IInvite3PID {
id_server: string;
id_access_token?: string; // this gets injected by the js-sdk
medium: string;
address: string;
}
export interface ICreateRoomStateEvent {
type: string;
state_key?: string; // defaults to an empty string
content: IContent;
}
export interface ICreateRoomOpts {
// The alias localpart to assign to this room.
room_alias_name?: string;
// Either 'public' or 'private'.
visibility?: Visibility;
// The name to give this room.
name?: string;
// The topic to give this room.
topic?: string;
preset?: Preset;
power_level_content_override?: {
ban?: number;
events?: Record<EventType | string, number>;
events_default?: number;
invite?: number;
kick?: number;
notifications?: Record<string, number>;
redact?: number;
state_default?: number;
users?: Record<string, number>;
users_default?: number;
};
creation_content?: object;
initial_state?: ICreateRoomStateEvent[];
// A list of user IDs to invite to this room.
invite?: string[];
invite_3pid?: IInvite3PID[];
is_direct?: boolean;
room_version?: string;
}
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?: {
// String to search for
generic_search_term?: string;
room_types?: Array<RoomType | null>;
};
include_all_networks?: boolean;
third_party_instance_id?: string;
}
export interface IAddThreePidOnlyBody {
auth?: {
type: string;
session?: string;
};
client_secret: string;
sid: string;
}
export interface IBindThreePidBody {
client_secret: string;
id_server: string;
// Some older identity servers have no auth enabled
id_access_token: string | null;
sid: string;
}
export interface IRelationsRequestOpts {
from?: string;
to?: string;
limit?: number;
dir?: Direction;
recurse?: boolean; // MSC3981 Relations Recursion https://github.com/matrix-org/matrix-spec-proposals/pull/3981
}
export interface IRelationsResponse {
chunk: IEvent[];
next_batch?: string;
prev_batch?: string;
}
export interface IContextResponse {
end?: string;
start?: string;
state?: IEventWithRoomId[];
events_before?: IEventWithRoomId[];
events_after?: IEventWithRoomId[];
event?: IEventWithRoomId;
}
export interface IEventsResponse {
chunk: IEventWithRoomId[];
end: string;
start: string;
}
export interface INotification {
actions: PushRuleAction[];
event: IRoomEvent;
profile_tag?: string;
read: boolean;
room_id: string;
ts: number;
}
export interface INotificationsResponse {
next_token: string;
notifications: INotification[];
}
export interface IFilterResponse {
filter_id: string;
}
export interface ITagsResponse {
tags: {
[tagId: string]: {
order: number;
};
};
}
export interface IStatusResponse extends IPresenceOpts {
currently_active?: boolean;
last_active_ago?: number;
}

16
node_modules/matrix-js-sdk/src/@types/retention.ts generated vendored Normal file
View File

@@ -0,0 +1,16 @@
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;
}

117
node_modules/matrix-js-sdk/src/@types/search.ts generated vendored Normal file
View File

@@ -0,0 +1,117 @@
/*
Copyright 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.
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.
*/
// 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";
export interface IEventWithRoomId extends IRoomEvent {
room_id: string;
}
export interface IStateEventWithRoomId extends IStateEvent {
room_id: string;
}
export interface IMatrixProfile {
avatar_url?: string;
displayname?: string;
}
export interface IResultContext {
events_before: IEventWithRoomId[];
events_after: IEventWithRoomId[];
profile_info: Record<string, IMatrixProfile>;
start?: string;
end?: string;
}
export interface ISearchResult {
rank: number;
result: IEventWithRoomId;
context: IResultContext;
}
enum GroupKey {
RoomId = "room_id",
Sender = "sender",
}
export interface IResultRoomEvents {
count?: number;
highlights?: string[];
results?: ISearchResult[];
state?: { [roomId: string]: IStateEventWithRoomId[] };
groups?: {
[groupKey in GroupKey]: {
[value: string]: {
next_batch?: string;
order: number;
results: string[];
};
};
};
next_batch?: string;
}
interface IResultCategories {
room_events: IResultRoomEvents;
}
export type SearchKey = "content.body" | "content.name" | "content.topic";
export enum SearchOrderBy {
Recent = "recent",
Rank = "rank",
}
export interface ISearchRequestBody {
search_categories: {
room_events: {
search_term: string;
keys?: SearchKey[];
filter?: IRoomEventFilter;
order_by?: SearchOrderBy;
event_context?: {
before_limit?: number;
after_limit?: number;
include_profile?: boolean;
};
include_state?: boolean;
groupings?: {
group_by: {
key: GroupKey;
}[];
};
};
};
}
export interface ISearchResponse {
search_categories: IResultCategories;
}
export interface ISearchResults {
_query?: ISearchRequestBody;
results: SearchResult[];
highlights: string[];
count?: number;
next_batch?: string;
pendingRequest?: Promise<ISearchResults>;
abortSignal?: AbortSignal;
}

25
node_modules/matrix-js-sdk/src/@types/signed.ts generated vendored Normal file
View File

@@ -0,0 +1,25 @@
/*
Copyright 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.
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 interface ISignatures {
[entity: string]: {
[keyId: string]: string;
};
}
export interface ISigned {
signatures?: ISignatures;
}

35
node_modules/matrix-js-sdk/src/@types/spaces.ts generated vendored Normal file
View File

@@ -0,0 +1,35 @@
/*
Copyright 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.
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 IPublicRoomsChunkRoom } from "../client.ts";
import { type RoomType } from "./event.ts";
import { type IStrippedState } from "../sync-accumulator.ts";
// Types relating to Rooms of type `m.space` and related APIs
export interface IHierarchyRelation extends IStrippedState {
origin_server_ts: number;
content: {
order?: string;
suggested?: boolean;
via?: string[];
};
}
export interface IHierarchyRoom extends IPublicRoomsChunkRoom {
room_type?: RoomType | string;
children_state: IHierarchyRelation[];
}

153
node_modules/matrix-js-sdk/src/@types/state_events.ts generated vendored Normal file
View File

@@ -0,0 +1,153 @@
/*
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;
}

38
node_modules/matrix-js-sdk/src/@types/synapse.ts generated vendored Normal file
View File

@@ -0,0 +1,38 @@
/*
Copyright 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.
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 IdServerUnbindResult } from "./partials.ts";
// Types relating to Synapse Admin APIs
export interface ISynapseAdminWhoisResponse {
user_id: string;
devices: {
[deviceId: string]: {
sessions: {
connections: {
ip: string;
last_seen: number; // millis since epoch
user_agent: string;
}[];
}[];
};
};
}
export interface ISynapseAdminDeactivateResponse {
id_server_unbind_result: IdServerUnbindResult;
}

27
node_modules/matrix-js-sdk/src/@types/sync.ts generated vendored Normal file
View File

@@ -0,0 +1,27 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ServerControlledNamespacedValue } from "../NamespacedValue.ts";
/**
* https://github.com/matrix-org/matrix-doc/pull/3773
*
* @experimental
*/
export const UNREAD_THREAD_NOTIFICATIONS = new ServerControlledNamespacedValue(
"unread_thread_notifications",
"org.matrix.msc3773.unread_thread_notifications",
);

29
node_modules/matrix-js-sdk/src/@types/threepids.ts generated vendored Normal file
View File

@@ -0,0 +1,29 @@
/*
Copyright 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum ThreepidMedium {
Email = "email",
Phone = "msisdn",
}
// TODO: Are these types universal, or specific to just /account/3pid?
export interface IThreepid {
medium: ThreepidMedium;
address: string;
validated_at: number;
added_at: number;
bound?: boolean;
}

69
node_modules/matrix-js-sdk/src/@types/topic.ts generated vendored Normal file
View File

@@ -0,0 +1,69 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { NamespacedValue } from "../NamespacedValue.ts";
import { type IMessageRendering } from "./extensible_events.ts";
/**
* Extensible topic event type based on MSC3765
* https://github.com/matrix-org/matrix-spec-proposals/pull/3765
*
* @example
* ```
* {
* "type": "m.room.topic,
* "state_key": "",
* "content": {
* "topic": "All about **pizza**",
* "m.topic": [{
* "body": "All about **pizza**",
* "mimetype": "text/plain",
* }, {
* "body": "All about <b>pizza</b>",
* "mimetype": "text/html",
* }],
* }
* }
* ```
*/
/**
* The event type for an m.topic event (in content)
*/
export const M_TOPIC = new NamespacedValue("m.topic", null);
/**
* 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[] };
/**
* The event definition for an m.topic event (in content)
*/
export type MTopicEvent = { "m.topic": MTopicContent } | MalformedMTopicEvent;
/**
* The event content for an m.room.topic event
*/
export type MRoomTopicEventContent = {
topic: string | null | undefined;
} & Partial<MTopicEvent>;

126
node_modules/matrix-js-sdk/src/@types/type-guards.ts generated vendored Normal file
View File

@@ -0,0 +1,126 @@
/*
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;
};

24
node_modules/matrix-js-sdk/src/@types/uia.ts generated vendored Normal file
View File

@@ -0,0 +1,24 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type AuthDict } from "../interactive-auth.ts";
/**
* Helper type to represent HTTP request body for a UIA enabled endpoint
*/
export type UIARequest<T> = T & {
auth?: AuthDict;
};

121
node_modules/matrix-js-sdk/src/NamespacedValue.ts generated vendored Normal file
View File

@@ -0,0 +1,121 @@
/*
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* 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.
*/
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,
) {
if (!this.unstable && !this.stable) {
throw new Error("One of stable or unstable values must be supplied");
}
}
public get name(): U | S {
if (this.stable) {
return this.stable;
}
return this.unstable!;
}
public get altName(): U | S | null | undefined {
if (!this.stable) {
return null;
}
return this.unstable;
}
public get names(): (U | S)[] {
const names = [this.name];
const altName = this.altName;
if (altName) names.push(altName);
return names;
}
public matches(val: string): boolean {
return this.name === val || this.altName === val;
}
// 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;
if (this.name) {
val = obj?.[this.name];
}
if (!val && this.altName) {
val = obj?.[this.altName];
}
return val;
}
public includedIn(arr: any[]): boolean {
let included = false;
if (this.name) {
included = arr.includes(this.name);
}
if (!included && this.altName) {
included = arr.includes(this.altName);
}
return included;
}
}
export class ServerControlledNamespacedValue<S extends string, U extends string> extends NamespacedValue<S, U> {
private preferUnstable = false;
public setPreferUnstable(preferUnstable: boolean): void {
this.preferUnstable = preferUnstable;
}
public get name(): U | S {
if (this.stable && !this.preferUnstable) {
return this.stable;
}
return this.unstable!;
}
}
/**
* Represents a namespaced value which prioritizes the unstable value over the stable
* value.
*/
export class UnstableValue<S extends string, U extends string> extends NamespacedValue<S, U> {
// Note: Constructor difference is that `unstable` is *required*.
public constructor(stable: S, unstable: U) {
super(stable, unstable);
if (!this.unstable) {
throw new Error("Unstable value must be supplied");
}
}
public get name(): U {
return this.unstable!;
}
public get altName(): S {
return this.stable!;
}
}

93
node_modules/matrix-js-sdk/src/ReEmitter.ts generated vendored Normal file
View File

@@ -0,0 +1,93 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 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.
*/
// eslint-disable-next-line no-restricted-imports
import { type EventEmitter } from "events";
import { type ListenerMap, type TypedEventEmitter } from "./models/typed-event-emitter.ts";
export class ReEmitter {
public constructor(private readonly target: EventEmitter) {}
// Map from emitter to event name to re-emitter
private reEmitters = new WeakMap<EventEmitter, Map<string, (...args: any[]) => void>>();
public reEmit(source: EventEmitter, eventNames: string[]): void {
let reEmittersByEvent = this.reEmitters.get(source);
if (!reEmittersByEvent) {
reEmittersByEvent = new Map();
this.reEmitters.set(source, reEmittersByEvent);
}
for (const eventName of eventNames) {
if (reEmittersByEvent.has(eventName)) continue;
// We include the source as the last argument for event handlers which may need it,
// such as read receipt listeners on the client class which won't have the context
// of the room.
const forSource = (...args: any[]): void => {
// EventEmitter special cases 'error' to make the emit function throw if no
// handler is attached, which sort of makes sense for making sure that something
// handles an error, but for re-emitting, there could be a listener on the original
// source object so the test doesn't really work. We *could* try to replicate the
// same logic and throw if there is no listener on either the source or the target,
// but this behaviour is fairly undesireable for us anyway: the main place we throw
// 'error' events is for calls, where error events are usually emitted some time
// later by a different part of the code where 'emit' throwing because the app hasn't
// added an error handler isn't terribly helpful. (A better fix in retrospect may
// have been to just avoid using the event name 'error', but backwards compat...)
if (eventName === "error" && this.target.listenerCount("error") === 0) return;
this.target.emit(eventName, ...args, source);
};
source.on(eventName, forSource);
reEmittersByEvent.set(eventName, forSource);
}
}
public stopReEmitting(source: EventEmitter, eventNames: string[]): void {
const reEmittersByEvent = this.reEmitters.get(source);
if (!reEmittersByEvent) return; // We were never re-emitting these events in the first place
for (const eventName of eventNames) {
source.off(eventName, reEmittersByEvent.get(eventName)!);
reEmittersByEvent.delete(eventName);
}
if (reEmittersByEvent.size === 0) this.reEmitters.delete(source);
}
}
export class TypedReEmitter<Events extends string, Arguments extends ListenerMap<Events>> extends ReEmitter {
public constructor(target: TypedEventEmitter<Events, Arguments>) {
super(target);
}
public reEmit<ReEmittedEvents extends string, T extends Events & ReEmittedEvents>(
source: TypedEventEmitter<ReEmittedEvents, any>,
eventNames: T[],
): void {
super.reEmit(source, eventNames);
}
public stopReEmitting<ReEmittedEvents extends string, T extends Events & ReEmittedEvents>(
source: TypedEventEmitter<ReEmittedEvents, any>,
eventNames: T[],
): void {
super.stopReEmitting(source, eventNames);
}
}

154
node_modules/matrix-js-sdk/src/ToDeviceMessageQueue.ts generated vendored Normal file
View File

@@ -0,0 +1,154 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { 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";
const MAX_BATCH_SIZE = 20;
/**
* Maintains a queue of outgoing to-device messages, sending them
* as soon as the homeserver is reachable.
*/
export class ToDeviceMessageQueue {
private sending = false;
private running = true;
private retryTimeout: ReturnType<typeof setTimeout> | null = null;
private retryAttempts = 0;
public constructor(
private client: MatrixClient,
private readonly logger: Logger,
) {}
public start(): void {
this.running = true;
this.sendQueue();
this.client.on(ClientEvent.Sync, this.onResumedSync);
}
public stop(): void {
this.running = false;
if (this.retryTimeout !== null) clearTimeout(this.retryTimeout);
this.retryTimeout = null;
this.client.removeListener(ClientEvent.Sync, this.onResumedSync);
}
public async queueBatch(batch: ToDeviceBatch): Promise<void> {
const batches: ToDeviceBatchWithTxnId[] = [];
for (let i = 0; i < batch.batch.length; i += MAX_BATCH_SIZE) {
const batchWithTxnId = {
eventType: batch.eventType,
batch: batch.batch.slice(i, i + MAX_BATCH_SIZE),
txnId: this.client.makeTxnId(),
};
batches.push(batchWithTxnId);
const msgmap = batchWithTxnId.batch.map(
(msg) => `${msg.userId}/${msg.deviceId} (msgid ${msg.payload[ToDeviceMessageId]})`,
);
this.logger.info(
`Enqueuing batch of to-device messages. type=${batch.eventType} txnid=${batchWithTxnId.txnId}`,
msgmap,
);
}
await this.client.store.saveToDeviceBatches(batches);
void this.sendQueue();
}
public sendQueue = async (): Promise<void> => {
if (this.retryTimeout !== null) clearTimeout(this.retryTimeout);
this.retryTimeout = null;
if (this.sending || !this.running) return;
this.logger.debug("Attempting to send queued to-device messages");
this.sending = true;
let headBatch: IndexedToDeviceBatch | null;
try {
while (this.running) {
headBatch = await this.client.store.getOldestToDeviceBatch();
if (headBatch === null) break;
await this.sendBatch(headBatch);
await this.client.store.removeToDeviceBatch(headBatch.id);
this.retryAttempts = 0;
}
// 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");
} catch (e) {
++this.retryAttempts;
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);
await this.client.store.removeToDeviceBatch(headBatch!.id);
} else {
this.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);
this.retryTimeout = setTimeout(this.sendQueue, retryDelay);
} finally {
this.sending = false;
}
};
/**
* Attempts to send a batch of to-device messages.
*/
private async sendBatch(batch: IndexedToDeviceBatch): Promise<void> {
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
for (const item of batch.batch) {
contentMap.getOrCreate(item.userId).set(item.deviceId, item.payload);
}
this.logger.info(
`Sending batch of ${batch.batch.length} to-device messages with ID ${batch.id} and txnId ${batch.txnId}`,
);
await this.client.sendToDevice(batch.eventType, contentMap, batch.txnId);
}
/**
* Listen to sync state changes and automatically resend any pending events
* once syncing is resumed
*/
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();
}
};
}

505
node_modules/matrix-js-sdk/src/autodiscovery.ts generated vendored Normal file
View File

@@ -0,0 +1,505 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 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";
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
export enum AutoDiscoveryAction {
SUCCESS = "SUCCESS",
IGNORE = "IGNORE",
PROMPT = "PROMPT",
FAIL_PROMPT = "FAIL_PROMPT",
FAIL_ERROR = "FAIL_ERROR",
}
export enum AutoDiscoveryError {
Invalid = "Invalid homeserver discovery response",
GenericFailure = "Failed to get autodiscovery configuration from server",
InvalidHsBaseUrl = "Invalid base_url for m.homeserver",
InvalidHomeserver = "Homeserver URL does not appear to be a valid Matrix homeserver",
InvalidIsBaseUrl = "Invalid base_url for m.identity_server",
InvalidIdentityServer = "Identity server URL does not appear to be a valid identity server",
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 {
state: AutoDiscoveryAction;
error?: IWellKnownConfig["error"] | null;
}
interface WellKnownConfig extends Omit<IWellKnownConfig, "error">, AutoDiscoveryState {}
export interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
"m.homeserver": WellKnownConfig;
"m.identity_server": WellKnownConfig;
}
/**
* Utilities for automatically discovery resources, such as homeservers
* for users to log in to.
*/
export class AutoDiscovery {
// Dev note: the constants defined here are related to but not
// exactly the same as those in the spec. This is to hopefully
// translate the meaning of the states in the spec, but also
// support our own if needed.
public static readonly ERROR_INVALID = AutoDiscoveryError.Invalid;
public static readonly ERROR_GENERIC_FAILURE = AutoDiscoveryError.GenericFailure;
public static readonly ERROR_INVALID_HS_BASE_URL = AutoDiscoveryError.InvalidHsBaseUrl;
public static readonly ERROR_INVALID_HOMESERVER = AutoDiscoveryError.InvalidHomeserver;
public static readonly ERROR_INVALID_IS_BASE_URL = AutoDiscoveryError.InvalidIsBaseUrl;
public static readonly ERROR_INVALID_IDENTITY_SERVER = AutoDiscoveryError.InvalidIdentityServer;
public static readonly ERROR_INVALID_IS = AutoDiscoveryError.InvalidIs;
public static readonly ERROR_MISSING_WELLKNOWN = AutoDiscoveryError.MissingWellknown;
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[];
/**
* The auto discovery failed. The client is expected to communicate
* the error to the user and refuse logging in.
*/
public static readonly FAIL_ERROR = AutoDiscoveryAction.FAIL_ERROR;
/**
* The auto discovery failed, however the client may still recover
* from the problem. The client is recommended to that the same
* action it would for PROMPT while also warning the user about
* what went wrong. The client may also treat this the same as
* a FAIL_ERROR state.
*/
public static readonly FAIL_PROMPT = AutoDiscoveryAction.FAIL_PROMPT;
/**
* The auto discovery didn't fail but did not find anything of
* interest. The client is expected to prompt the user for more
* information, or fail if it prefers.
*/
public static readonly PROMPT = AutoDiscoveryAction.PROMPT;
/**
* The auto discovery was successful.
*/
public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS;
/**
* Validates and verifies client configuration information for purposes
* of logging in. Such information includes the homeserver URL
* and identity server URL the client would want. Additional details
* may also be included, and will be transparently brought into the
* response object unaltered.
* @param wellknown - The configuration object itself, as returned
* by the .well-known auto-discovery endpoint.
* @returns Promise which resolves to the verified
* configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails.
*/
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
// write. We'll update the properties of this object over the duration
// of this function.
const clientConfig: ClientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
// Technically, we don't have a problem with the identity server
// config at this point.
state: AutoDiscovery.PROMPT,
error: null,
base_url: null,
},
};
if (!wellknown?.["m.homeserver"]) {
logger.error("No m.homeserver key in config");
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
return Promise.resolve(clientConfig);
}
if (!wellknown["m.homeserver"]["base_url"]) {
logger.error("No m.homeserver base_url in config");
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
return Promise.resolve(clientConfig);
}
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3.
const hsUrl = this.sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]);
if (!hsUrl) {
logger.error("Invalid base_url for m.homeserver");
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
return Promise.resolve(clientConfig);
}
// 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"])) {
logger.error("Invalid /versions response");
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
// 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 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,
error: null,
base_url: hsUrl,
};
// Step 5: Try to pull out the identity server configuration
let isUrl: string | boolean = "";
if (wellknown["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch.
const failingClientConfig: ClientConfig = {
"m.homeserver": clientConfig["m.homeserver"],
"m.identity_server": {
state: AutoDiscovery.FAIL_PROMPT,
error: AutoDiscovery.ERROR_INVALID_IS,
base_url: null,
},
};
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this.sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]);
if (!isUrl) {
logger.error("Invalid base_url for m.identity_server");
failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IS_BASE_URL;
return Promise.resolve(failingClientConfig);
}
// Step 5b: Verify there is an identity server listening on the provided
// URL.
const isResponse = await this.fetchWellKnownObject(`${isUrl}/_matrix/identity/v2`);
if (!isResponse?.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
logger.error("Invalid /v2 response");
failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
// Supply the base_url to the caller because they may be ignoring
// liveliness errors, like this one.
failingClientConfig["m.identity_server"].base_url = isUrl;
return Promise.resolve(failingClientConfig);
}
}
// Step 6: Now that the identity server is valid, or never existed,
// populate the IS section.
if (isUrl && isUrl.toString().length > 0) {
clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: isUrl,
};
}
// Step 7: Copy any other keys directly into the clientConfig. This is for
// things like custom configuration of services.
Object.keys(wellknown).forEach((k: keyof IClientWellKnown) => {
if (k === "m.homeserver" || k === "m.identity_server") {
// Only copy selected parts of the config to avoid overwriting
// properties computed by the validation logic above.
const notProps = ["error", "state", "base_url"];
for (const prop of Object.keys(wellknown[k]!)) {
if (notProps.includes(prop)) continue;
type Prop = Exclude<keyof IWellKnownConfig, "error" | "state" | "base_url">;
// @ts-ignore - ts gets unhappy as we're mixing types here
clientConfig[k][prop as Prop] = wellknown[k]![prop as Prop];
}
} else {
// Just copy the whole thing over otherwise
clientConfig[k] = wellknown[k];
}
});
// Step 8: Give the config to the caller (finally)
return Promise.resolve(clientConfig);
}
/**
* Attempts to automatically discover client configuration information
* prior to logging in. Such information includes the homeserver URL
* and identity server URL the client would want. Additional details
* may also be discovered, and will be transparently included in the
* response object unaltered.
* @param domain - The homeserver domain to perform discovery
* on. For example, "matrix.org".
* @returns Promise which resolves to the discovered
* configuration, which may include error states. Rejects on unexpected
* failure, not when discovery fails.
*/
public static async findClientConfig(domain: string): Promise<ClientConfig> {
if (!domain || typeof domain !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
// We use a .well-known lookup for all cases. According to the spec, we
// can do other discovery mechanisms if we want such as custom lookups
// however we won't bother with that here (mostly because the spec only
// supports .well-known right now).
//
// By using .well-known, we need to ensure we at least pull out a URL
// for the homeserver. We don't really need an identity server configuration
// but will return one anyways (with state PROMPT) to make development
// easier for clients. If we can't get a homeserver URL, all bets are
// off on the rest of the config and we'll assume it is invalid too.
// We default to an error state to make the first few checks easier to
// write. We'll update the properties of this object over the duration
// of this function.
const clientConfig: ClientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
// Technically, we don't have a problem with the identity server
// config at this point.
state: AutoDiscovery.PROMPT,
error: null,
base_url: null,
},
};
// Step 1: Actually request the .well-known JSON file and make sure it
// at least has a homeserver definition.
const domainWithProtocol = domain.includes("://") ? domain : `https://${domain}`;
const wellknown = await this.fetchWellKnownObject(`${domainWithProtocol}/.well-known/matrix/client`);
if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) {
logger.error("No response or error when parsing .well-known");
if (wellknown.reason) logger.error(wellknown.reason);
if (wellknown.action === AutoDiscoveryAction.IGNORE) {
clientConfig["m.homeserver"] = {
state: AutoDiscovery.PROMPT,
error: null,
base_url: null,
};
} else {
// this can only ever be FAIL_PROMPT at this point.
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
}
return Promise.resolve(clientConfig);
}
// Step 2: Validate and parse the config
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw);
}
/**
* Gets the raw discovery client configuration for the given domain name.
* Should only be used if there's no validation to be done on the resulting
* object, otherwise use findClientConfig().
* @param domain - The domain to get the client config for.
* @returns Promise which resolves to the domain's client config. Can
* be an empty object.
*/
public static async getRawClientConfig(domain?: string): Promise<IClientWellKnown> {
if (!domain || typeof domain !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
const response = await this.fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`);
if (!response) return {};
return response.raw ?? {};
}
/**
* Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
* is suitable for the requirements laid out by .well-known auto discovery.
* If valid, the URL will also be stripped of any trailing slashes.
* @param url - The potentially invalid URL to sanitize.
* @returns The sanitized URL or a falsey value if the URL is invalid.
* @internal
*/
private static sanitizeWellKnownUrl(url?: string | null): string | false {
if (!url) return false;
try {
let parsed: URL | undefined;
try {
parsed = new URL(url);
} catch (e) {
logger.error("Could not parse url", e);
}
if (!parsed?.hostname) return false;
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
const port = parsed.port ? `:${parsed.port}` : "";
const path = parsed.pathname ? parsed.pathname : "";
let saferUrl = `${parsed.protocol}//${parsed.hostname}${port}${path}`;
if (saferUrl.endsWith("/")) {
saferUrl = saferUrl.substring(0, saferUrl.length - 1);
}
return saferUrl;
} catch (e) {
logger.error(e);
return false;
}
}
private static fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof globalThis.fetch> {
if (this.fetchFn) {
return this.fetchFn(resource, options);
}
return globalThis.fetch(resource, options);
}
private static fetchFn?: typeof globalThis.fetch;
public static setFetchFn(fetchFn: typeof globalThis.fetch): void {
AutoDiscovery.fetchFn = fetchFn;
}
/**
* Fetches a JSON object from a given URL, as expected by all .well-known
* related lookups. If the server gives a 404 then the `action` will be
* IGNORE. If the server returns something that isn't JSON, the `action`
* will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT.
*
* The returned object will be a result of the call in object form with
* the following properties:
* raw: The JSON object returned by the server.
* action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
* reason: Relatively human-readable description of what went wrong.
* error: The actual Error, if one exists.
* @param url - The URL to fetch a JSON object from.
* @returns Promise which resolves to the returned state.
* @internal
*/
private static async fetchWellKnownObject<T = IWellKnownConfig>(
url: string,
): Promise<IWellKnownConfig<Partial<T>>> {
let response: Response;
try {
response = await AutoDiscovery.fetch(url, {
method: Method.Get,
signal: timeoutSignal(5000),
});
if (response.status === 404) {
return {
raw: {},
action: AutoDiscoveryAction.IGNORE,
reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN,
};
}
if (response.status !== 200) {
return {
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: "General failure",
};
}
} catch (err) {
const error = err as AutoDiscoveryError | string | undefined;
let reason = "";
if (typeof error === "object") {
reason = (<Error>error)?.message;
}
return {
error,
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: reason || "General failure",
};
}
try {
return {
raw: await response.json(),
action: AutoDiscoveryAction.SUCCESS,
};
} catch (err) {
const error = err as Error;
return {
error,
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason:
(error as MatrixError)?.name === "SyntaxError"
? AutoDiscovery.ERROR_INVALID_JSON
: AutoDiscovery.ERROR_INVALID,
};
}
}
}

86
node_modules/matrix-js-sdk/src/base64.ts generated vendored Normal file
View File

@@ -0,0 +1,86 @@
/*
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" });
}

44
node_modules/matrix-js-sdk/src/browser-index.ts generated vendored Normal file
View File

@@ -0,0 +1,44 @@
/*
Copyright 2019 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 * as sdk from "./matrix.ts";
type BrowserMatrix = typeof sdk;
declare global {
var __js_sdk_entrypoint: boolean;
var matrixcs: BrowserMatrix;
}
if (globalThis.__js_sdk_entrypoint) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
}
globalThis.__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
}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
if (indexedDB) {
sdk.setCryptoStoreFactory(() => new sdk.IndexedDBCryptoStore(indexedDB, "matrix-js-sdk:crypto"));
}
export * from "./matrix.ts";
globalThis.matrixcs = sdk;

102
node_modules/matrix-js-sdk/src/capabilityPoller.ts generated vendored Normal file
View File

@@ -0,0 +1,102 @@
/*
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;
}
}
}

9033
node_modules/matrix-js-sdk/src/client.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,307 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import 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";
/**
* Common interface for the crypto implementations
*
* @internal
*/
export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
/**
* Whether sendMessage in a room with unknown and unverified devices
* should throw an error and not send the message. This has 'Global' for
* 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.
*/
globalErrorOnUnknownDevices: boolean;
/**
* Shut down any background processes related to crypto
*/
stop(): void;
/**
* Encrypt an event according to the configuration of the room.
*
* @param event - event to be sent
*
* @param room - destination room.
*
* @returns Promise which resolves when the event has been
* encrypted, or null if nothing was needed
*/
encryptEvent(event: MatrixEvent, room: Room): Promise<void>;
/**
* Decrypt a received event
*
* @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>;
/**
* 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.
*/
getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise<BackupDecryptor>;
/**
* Import a list of room keys restored from backup
*
* @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
*/
importBackedUpRoomKeys(keys: IMegolmSessionData[], backupVersion: string, opts?: ImportRoomKeysOpts): Promise<void>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Room key history sharing (MSC4268)
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* 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)
*/
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>;
}
/** The methods which crypto implementations should expose to the Sync api
*
* @internal
*/
export interface SyncCryptoCallbacks {
/**
* Called by the /sync loop whenever there are incoming to-device messages.
*
* The implementation may preprocess the received messages (eg, decrypt them) and return an
* updated list of messages for dispatch to the rest of the system.
*
* Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device
* 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.
*
*/
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<ReceivedToDeviceMessage[]>;
/**
* Called by the /sync loop when one time key counts and unused fallback key details are received.
*
* @param oneTimeKeysCounts - the received one time key counts
* @param unusedFallbackKeys - the received unused fallback keys
*/
processKeyCounts(oneTimeKeysCounts?: Record<string, number>, unusedFallbackKeys?: string[]): Promise<void>;
/**
* Handle the notification from /sync that device lists have
* been changed.
*
* @param deviceLists - device_lists field from /sync
*/
processDeviceLists(deviceLists: IDeviceLists): Promise<void>;
/**
* Called by the /sync loop whenever an m.room.encryption event is received.
*
* This is called before RoomStateEvents are emitted for any of the events in the /sync
* response (even if the other events technically happened first). This works around a problem
* if the client uses a RoomStateEvent (typically a membership event) as a trigger to send a message
* in a new room (or one where encryption has been newly enabled): that would otherwise leave the
* crypto layer confused because it expects crypto to be set up, but it has not yet been.
*
* @param room - in which the event was received
* @param event - encryption event to be processed
*/
onCryptoEvent(room: Room, event: MatrixEvent): Promise<void>;
/**
* Called by the /sync loop after each /sync response is processed.
*
* Used to complete batch processing, or to initiate background processes
*
* @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>;
}
/**
* @internal
*/
export interface OnSyncCompletedData {
/**
* The 'next_batch' result from /sync, which will become the 'since' token for the next call to /sync.
*/
nextSyncToken?: string;
/**
* True if we are working our way through a backlog of events after connecting.
*/
catchingUp?: boolean;
}
/**
* The result of a (successful) call to {@link CryptoBackend.decryptEvent}
*/
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;
}

View File

@@ -0,0 +1,4 @@
This directory contains functionality which is common to both the legacy (libolm-based) crypto implementation,
and the new rust-based implementation.
It is an internal module, and is _not_ directly exposed to applications.

View File

@@ -0,0 +1,42 @@
/*
* 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,
);
}

298
node_modules/matrix-js-sdk/src/content-helpers.ts generated vendored Normal file
View File

@@ -0,0 +1,298 @@
/*
Copyright 2018 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { 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 {
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";
/**
* Generates the content for a HTML Message event
* @param body - the plaintext body of the message
* @param htmlBody - the HTML representation of the message
* @returns
*/
export function makeHtmlMessage(body: string, htmlBody: string): RoomMessageEventContent {
return {
msgtype: MsgType.Text,
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
}
/**
* Generates the content for a HTML Notice event
* @param body - the plaintext body of the notice
* @param htmlBody - the HTML representation of the notice
* @returns
*/
export function makeHtmlNotice(body: string, htmlBody: string): RoomMessageEventContent {
return {
msgtype: MsgType.Notice,
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
}
/**
* Generates the content for a HTML Emote event
* @param body - the plaintext body of the emote
* @param htmlBody - the HTML representation of the emote
* @returns
*/
export function makeHtmlEmote(body: string, htmlBody: string): RoomMessageEventContent {
return {
msgtype: MsgType.Emote,
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
}
/**
* Generates the content for a Plaintext Message event
* @param body - the plaintext body of the emote
* @returns
*/
export function makeTextMessage(body: string): RoomMessageEventContent {
return {
msgtype: MsgType.Text,
body: body,
};
}
/**
* Generates the content for a Plaintext Notice event
* @param body - the plaintext body of the notice
* @returns
*/
export function makeNotice(body: string): RoomMessageEventContent {
return {
msgtype: MsgType.Notice,
body: body,
};
}
/**
* Generates the content for a Plaintext Emote event
* @param body - the plaintext body of the emote
* @returns
*/
export function makeEmoteMessage(body: string): RoomMessageEventContent {
return {
msgtype: MsgType.Emote,
body: body,
};
}
/** Location content helpers */
export const getTextForLocationEvent = (
uri: string | undefined,
assetType: LocationAssetType,
timestamp?: number,
description?: string | null,
): string => {
const date = `at ${new Date(timestamp!).toISOString()}`;
const assetName = assetType === LocationAssetType.Self ? "User" : undefined;
const quotedDescription = description ? `"${description}"` : undefined;
return [assetName, "Location", quotedDescription, uri, date].filter(Boolean).join(" ");
};
/**
* Generates the content for a Location event
* @param uri - a geo:// uri for the location
* @param timestamp - the timestamp when the location was correct (milliseconds since the UNIX epoch)
* @param description - the (optional) label for this location on the map
* @param assetType - the (optional) asset type of this location e.g. "m.self"
* @param text - optional. A text for the location
*/
export const makeLocationContent = (
// this is first but optional
// to avoid a breaking change
text?: string,
uri?: string,
timestamp?: number,
description?: string | null,
assetType?: LocationAssetType,
): LegacyLocationEventContent & MLocationEventContent => {
const defaultedText =
text ?? getTextForLocationEvent(uri, assetType || LocationAssetType.Self, timestamp, description);
const timestampEvent = timestamp ? { [M_TIMESTAMP.name]: timestamp } : {};
return {
msgtype: MsgType.Location,
body: defaultedText,
geo_uri: uri,
[M_LOCATION.name]: {
description,
uri,
},
[M_ASSET.name]: {
type: assetType || LocationAssetType.Self,
},
[M_TEXT.name]: defaultedText,
...timestampEvent,
} as LegacyLocationEventContent & MLocationEventContent;
};
/**
* Parse location event content and transform to
* a backwards compatible modern m.location event format
*/
export const parseLocationEvent = (wireEventContent: LocationEventWireContent): MLocationEventContent => {
const location = M_LOCATION.findIn<MLocationContent>(wireEventContent);
const asset = M_ASSET.findIn<MAssetContent>(wireEventContent);
const timestamp = M_TIMESTAMP.findIn<number>(wireEventContent);
const text = M_TEXT.findIn<string>(wireEventContent);
const geoUri = location?.uri ?? wireEventContent?.geo_uri;
const description = location?.description;
const assetType = asset?.type ?? LocationAssetType.Self;
const fallbackText = text ?? wireEventContent.body;
return makeLocationContent(fallbackText, geoUri, timestamp ?? undefined, description, assetType);
};
/**
* Topic event helpers
*/
export type MakeTopicContent = (topic: string | null | undefined, 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
if (isProvided(htmlTopic)) {
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 } };
};
export type TopicState = {
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"];
if (!Array.isArray(mtopic)) {
return { text: content.topic ?? undefined };
}
const text =
mtopic?.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic ?? undefined;
const html = mtopic?.find((r) => r.mimetype === "text/html")?.body;
return { text, html };
};
/**
* Beacon event helpers
*/
export type MakeBeaconInfoContent = (
timeout: number,
isLive?: boolean,
description?: string,
assetType?: LocationAssetType,
timestamp?: number,
) => MBeaconInfoEventContent;
export const makeBeaconInfoContent: MakeBeaconInfoContent = (timeout, isLive, description, assetType, timestamp) => ({
description,
timeout,
live: isLive,
[M_TIMESTAMP.name]: timestamp || Date.now(),
[M_ASSET.name]: {
type: assetType ?? LocationAssetType.Self,
},
});
export type BeaconInfoState = MBeaconInfoContent & {
assetType?: LocationAssetType;
timestamp?: number;
};
/**
* Flatten beacon info event content
*/
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
const { description, timeout, live } = content;
const timestamp = M_TIMESTAMP.findIn<number>(content) ?? undefined;
const asset = M_ASSET.findIn<MAssetContent>(content);
return {
description,
timeout,
live,
assetType: asset?.type,
timestamp,
};
};
export type MakeBeaconContent = (
uri: string,
timestamp: number,
beaconInfoEventId: string,
description?: string,
) => MBeaconEventContent;
export const makeBeaconContent: MakeBeaconContent = (uri, timestamp, beaconInfoEventId, description) => ({
[M_LOCATION.name]: {
description,
uri,
},
[M_TIMESTAMP.name]: timestamp,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: beaconInfoEventId,
},
});
export type BeaconLocationState = Omit<MLocationContent, "uri"> & {
uri?: string; // override from MLocationContent to allow optionals
timestamp?: number;
};
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
const location = M_LOCATION.findIn<MLocationContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content) ?? undefined;
return {
description: location?.description,
uri: location?.uri,
timestamp,
};
};

122
node_modules/matrix-js-sdk/src/content-repo.ts generated vendored Normal file
View File

@@ -0,0 +1,122 @@
/*
Copyright 2015 - 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.
*/
// 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;
}
/**
* Get the HTTP URL for an MXC URI.
* @param baseUrl - The base homeserver url which has a content repo.
* @param mxc - The mxc:// URI.
* @param width - The desired width of the thumbnail.
* @param height - The desired height of the thumbnail.
* @param resizeMethod - The thumbnail resize method to use, either
* "crop" or "scale".
* @param allowDirectLinks - If true, return any non-mxc URLs
* 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.
*/
export function getHttpUriForMxc(
baseUrl: string,
mxc?: string,
width?: number,
height?: number,
resizeMethod?: string,
allowDirectLinks = false,
allowRedirects?: boolean,
useAuthentication?: boolean,
animated?: boolean,
): string {
if (typeof mxc !== "string" || !mxc) {
return "";
}
if (!mxc.startsWith("mxc://")) {
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);
if (width) {
url.searchParams.set("width", Math.round(width).toString());
}
if (height) {
url.searchParams.set("height", Math.round(height).toString());
}
if (resizeMethod) {
url.searchParams.set("method", resizeMethod);
}
if (animated !== undefined) {
url.searchParams.set("animated", String(animated));
}
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));
}
return url.href;
}

View File

@@ -0,0 +1,152 @@
/*
* 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",
}

View File

@@ -0,0 +1,42 @@
/*
* 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 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
/*
* 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);
}

121
node_modules/matrix-js-sdk/src/crypto-api/keybackup.ts generated vendored Normal file
View File

@@ -0,0 +1,121 @@
/*
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.
*/
import { type ISigned } from "../@types/signed.ts";
import { type AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
import { type ImportRoomKeyProgressData } from "./index.ts";
export interface Curve25519AuthData {
public_key: string;
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
export interface Aes256AuthData {
iv: string;
mac: string;
private_key_salt?: string;
private_key_iterations?: number;
}
/**
* 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).
*/
export interface NewKeyBackupInfo {
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;
}

View File

@@ -0,0 +1,69 @@
/*
* 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));
}

View File

@@ -0,0 +1,400 @@
/*
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.
*/
import { type MatrixEvent } from "../models/event.ts";
import { type TypedEventEmitter } from "../models/typed-event-emitter.ts";
/**
* An incoming, or outgoing, request to verify a user or a device via cross-signing.
*/
export interface VerificationRequest extends TypedEventEmitter<
VerificationRequestEvent,
VerificationRequestEventHandlerMap
> {
/**
* Unique ID for this verification request.
*
* An ID isn't assigned until the first message is sent, so this may be `undefined` in the early phases.
*/
get transactionId(): string | undefined;
/**
* For an in-room verification, the ID of the room.
*
* For to-device verifictions, `undefined`.
*/
get roomId(): string | undefined;
/**
* True if this request was initiated by the local client.
*
* For in-room verifications, the initiator is who sent the `m.key.verification.request` event.
* For to-device verifications, the initiator is who sent the `m.key.verification.start` event.
*/
get initiatedByMe(): boolean;
/** The user id of the other party in this request */
get otherUserId(): string;
/** For verifications via to-device messages: the ID of the other device. Otherwise, undefined. */
get otherDeviceId(): string | undefined;
/** True if the other party in this request is one of this user's own devices. */
get isSelfVerification(): boolean;
/** current phase of the request. */
get phase(): VerificationPhase;
/** True if the request has sent its initial event and needs more events to complete
* (ie it is in phase `Requested`, `Ready` or `Started`).
*/
get pending(): boolean;
/**
* True if we have started the process of sending an `m.key.verification.ready` (but have not necessarily received
* the remote echo which causes a transition to {@link VerificationPhase.Ready}.
*/
get accepting(): boolean;
/**
* True if we have started the process of sending an `m.key.verification.cancel` (but have not necessarily received
* the remote echo which causes a transition to {@link VerificationPhase.Cancelled}).
*/
get declining(): boolean;
/**
* The remaining number of ms before the request will be automatically cancelled.
*
* `null` indicates that there is no timeout
*/
get timeout(): number | null;
/** once the phase is Started (and !initiatedByMe) or Ready: common methods supported by both sides */
get methods(): string[];
/** the method picked in the .start event */
get chosenMethod(): string | null;
/**
* Checks whether the other party supports a given verification method.
* This is useful when setting up the QR code UI, as it is somewhat asymmetrical:
* if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa.
* For methods that need to be supported by both ends, use the `methods` property.
*
* @param method - the method to check
* @returns true if the other party said they supported the method
*/
otherPartySupportsMethod(method: string): boolean;
/**
* Accepts the request, sending a .ready event to the other party
*
* @returns Promise which resolves when the event has been sent.
*/
accept(): Promise<void>;
/**
* Cancels the request, sending a cancellation to the other party
*
* @param params - Details for the cancellation, including `reason` (defaults to "User declined"), and `code`
* (defaults to `m.user`). **Deprecated**: this parameter is ignored by the Rust cryptography implementation.
*
* @returns Promise which resolves when the event has been sent.
*/
cancel(params?: { reason?: string; code?: string }): Promise<void>;
/**
* 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.
*
* @param method - the name of the verification method to use.
*
* @returns The verifier which will do the actual verification.
*/
startVerification(method: string): Promise<Verifier>;
/**
* 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.
*
* 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>;
/**
* The verifier which is doing the actual verification, once the method has been established.
* Only defined when the `phase` is Started.
*/
get verifier(): Verifier | 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.
*/
generateQRCode(): Promise<Uint8ClampedArray | undefined>;
/**
* If this request has been cancelled, the cancellation code (e.g `m.user`) which is responsible for cancelling
* this verification.
*/
get cancellationCode(): string | null;
/**
* The id of the user that cancelled the request.
*
* Only defined when phase is Cancelled
*/
get cancellingUserId(): string | undefined;
}
/** Events emitted by {@link VerificationRequest}. */
export enum VerificationRequestEvent {
/**
* Fires whenever the state of the request object has changed.
*
* There is no payload to the event.
*/
Change = "change",
}
/**
* Listener type map for {@link VerificationRequestEvent}s.
*
* @internal
*/
export type VerificationRequestEventHandlerMap = {
[VerificationRequestEvent.Change]: () => void;
};
/** The current phase of a verification request. */
export enum VerificationPhase {
/** Initial state: no event yet exchanged */
Unsent = 1,
/** An `m.key.verification.request` event has been sent or received */
Requested,
/** 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.
*/
Started,
/**
* 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.
*/
Done,
}
/**
* A `Verifier` is responsible for performing the verification using a particular method, such as via QR code or SAS
* (emojis).
*
* A verifier object can be created by calling `VerificationRequest.beginVerification`; one is also created
* automatically when a `m.key.verification.start` event is received for an existing VerificationRequest.
*
* Once a verifier object is created, the verification can be started by calling the {@link Verifier#verify} method.
*/
export interface Verifier extends TypedEventEmitter<VerifierEvent, VerifierEventHandlerMap> {
/**
* Returns true if the verification has been cancelled, either by us or the other side.
*/
get hasBeenCancelled(): boolean;
/**
* The ID of the other user in the verification process.
*/
get userId(): string;
/**
* Start the key verification, if it has not already been started.
*
* This means sending a `m.key.verification.start` if we are the first responder, or a `m.key.verification.accept`
* if the other side has already sent a start event.
*
* @returns Promise which resolves when the verification has completed, or rejects if the verification is cancelled
* or times out.
*/
verify(): Promise<void>;
/**
* 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.
*
* @param e - the reason for the cancellation.
*/
cancel(e: Error): void;
/**
* Get the details for an SAS verification, if one is in progress
*
* Returns `null`, unless this verifier is for a SAS-based verification and we are waiting for the user to confirm
* the SAS matches.
*/
getShowSasCallbacks(): ShowSasCallbacks | null;
/**
* Get the details for reciprocating QR code verification, if one is in progress
*
* Returns `null`, unless this verifier is for reciprocating a QR-code-based verification (ie, the other user has
* already scanned our QR code), and we are waiting for the user to confirm.
*/
getReciprocateQrCodeCallbacks(): ShowQrCodeCallbacks | null;
}
/** Events emitted by {@link Verifier} */
export enum VerifierEvent {
/**
* The verification has been cancelled, by us or the other side.
*
* The payload is either an {@link Error}, or an (incoming or outgoing) {@link MatrixEvent}, depending on
* unspecified reasons.
*/
Cancel = "cancel",
/**
* SAS data has been exchanged and should be displayed to the user.
*
* The payload is the {@link ShowSasCallbacks} object.
*/
ShowSas = "show_sas",
/**
* The user should confirm if the other side has scanned our QR code.
*
* The payload is the {@link ShowQrCodeCallbacks} object.
*/
ShowReciprocateQr = "show_reciprocate_qr",
}
/** Listener type map for {@link VerifierEvent}s. */
export type VerifierEventHandlerMap = {
[VerifierEvent.Cancel]: (e: Error | MatrixEvent) => void;
[VerifierEvent.ShowSas]: (sas: ShowSasCallbacks) => void;
[VerifierEvent.ShowReciprocateQr]: (qr: ShowQrCodeCallbacks) => void;
};
/**
* Callbacks for user actions to confirm that the other side has scanned our QR code.
*
* This is exposed as the payload of a `VerifierEvent.ShowReciprocateQr` event, or can be retrieved directly from the
* verifier as `reciprocateQREvent`.
*/
export interface ShowQrCodeCallbacks {
/** The user confirms that the verification data matches */
confirm(): void;
/** Cancel the verification flow */
cancel(): void;
}
/**
* Callbacks for user actions while a SAS is displayed.
*
* This is exposed as the payload of a `VerifierEvent.ShowSas` event, or directly from the verifier as `sasEvent`.
*/
export interface ShowSasCallbacks {
/** The generated SAS to be shown to the user */
sas: GeneratedSas;
/** Function to call if the user confirms that the SAS matches.
*
* @returns A Promise that completes once the m.key.verification.mac is queued.
*/
confirm(): Promise<void>;
/**
* Function to call if the user finds the SAS does not match.
*
* Sends an `m.key.verification.cancel` event with a `m.mismatched_sas` error code.
*/
mismatch(): void;
/** Cancel the verification flow */
cancel(): void;
}
/** A generated SAS to be shown to the user, in alternative formats */
export interface GeneratedSas {
/**
* The SAS as three numbers between 0 and 8191.
*
* Only populated if the `decimal` SAS method was negotiated.
*/
decimal?: [number, number, number];
/**
* The SAS as seven emojis.
*
* Only populated if the `emoji` SAS method was negotiated.
*/
emoji?: EmojiMapping[];
}
/**
* An emoji for the generated SAS. A tuple `[emoji, name]` where `emoji` is the emoji itself and `name` is the
* English name.
*/
export type EmojiMapping = [emoji: string, name: string];
/**
* True if the request is in a state where it can be accepted (ie, that we're in phases {@link VerificationPhase.Unsent}
* or {@link VerificationPhase.Requested}, and that we're not in the process of sending a `ready` or `cancel`).
*/
export function canAcceptVerificationRequest(req: VerificationRequest): boolean {
return req.phase < VerificationPhase.Ready && !req.accepting && !req.declining;
}

381
node_modules/matrix-js-sdk/src/crypto/store/base.ts generated vendored Normal file
View File

@@ -0,0 +1,381 @@
/*
Copyright 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.
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 CrossSigningKeyInfo } from "../../crypto-api/index.ts";
import { type AESEncryptedSecretStoragePayload } from "../../@types/AESEncryptedSecretStoragePayload.ts";
import { type ISignatures } from "../../@types/signed.ts";
/**
* Internal module. Definitions for storage for the crypto module
*/
export interface SecretStorePrivateKeys {
"m.megolm_backup.v1": AESEncryptedSecretStoragePayload;
}
/**
* 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>;
// 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;
getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void;
storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
type: K,
key: SecretStorePrivateKeys[K],
): void;
// Olm Sessions
countEndToEndSessions(txn: unknown, func: (count: number) => void): void;
getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo | null) => void,
): void;
getEndToEndSessions(
deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => 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>;
// Inbound Group Sessions
getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: unknown,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): 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>;
// Device Data
getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void;
markSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
// Session key backups
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: Logger): 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;
session?: string;
lastReceivedMessageTs?: number;
}
export interface IDeviceData {
devices: {
[userId: string]: {
[deviceId: string]: IDevice;
};
};
trackingStatus: {
[userId: string]: TrackingStatus;
};
crossSigningInfo?: Record<string, ICrossSigningInfo>;
syncToken?: string;
}
export interface IWithheld {
room_id: string;
code: string;
reason: string;
}
/**
* Represents an outgoing room key request
*/
export interface OutgoingRoomKeyRequest {
/**
* Unique id for this request. Used for both an id within the request for later pairing with a cancellation,
* and for the transaction id when sending the to_device messages to our local server.
*/
requestId: string;
requestTxnId?: string;
/**
* Transaction id for the cancellation, if any
*/
cancellationTxnId?: string;
/**
* List of recipients for the request
*/
recipients: IRoomKeyRequestRecipient[];
/**
* Parameters for the request
*/
requestBody: IRoomKeyRequestBody;
/**
* current state of this request
*/
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). */
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,
}

View File

@@ -0,0 +1,656 @@
/*
Copyright 2017 - 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.
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, logger } from "../../logger.ts";
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";
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;
/**
*/
public constructor(private db: IDBDatabase) {
// make sure we close the db on `onversionchange` - otherwise
// attempts to delete the database will block (and subsequent
// attempts to re-create it will also block).
db.onversionchange = (): void => {
logger.log(`versionchange for indexeddb ${this.db.name}: closing`);
db.close();
};
}
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.
*
* Implementation of {@link CryptoStore.getMigrationState}.
*/
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;
};
});
return migrationState;
}
/**
* Set data on how much of the libolm to Rust Crypto migration has been done.
*
* Implementation of {@link CryptoStore.setMigrationState}.
*/
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);
});
}
// Olm Account
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("-");
getReq.onsuccess = function (): void {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, <Error>e);
}
};
}
public storeAccount(txn: IDBTransaction, accountPickle: string): void {
const objectStore = txn.objectStore("account");
objectStore.put(accountPickle, "-");
}
public getCrossSigningKeys(
txn: IDBTransaction,
func: (keys: Record<string, CrossSigningKeyInfo> | null) => void,
): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("crossSigningKeys");
getReq.onsuccess = function (): void {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, <Error>e);
}
};
}
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get(`ssss_cache:${type}`);
getReq.onsuccess = function (): void {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, <Error>e);
}
};
}
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
type: K,
key: SecretStorePrivateKeys[K],
): void {
const objectStore = txn.objectStore("account");
objectStore.put(key, `ssss_cache:${type}`);
}
// Olm Sessions
public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
const objectStore = txn.objectStore("sessions");
const countReq = objectStore.count();
countReq.onsuccess = function (): void {
try {
func(countReq.result);
} catch (e) {
abortWithException(txn, <Error>e);
}
};
}
public getEndToEndSessions(
deviceKey: string,
txn: IDBTransaction,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
const objectStore = txn.objectStore("sessions");
const idx = objectStore.index("deviceKey");
const getReq = idx.openCursor(deviceKey);
const results: Parameters<Parameters<Backend["getEndToEndSessions"]>[2]>[0] = {};
getReq.onsuccess = function (): void {
const cursor = getReq.result;
if (cursor) {
results[cursor.value.sessionId] = {
session: cursor.value.session,
lastReceivedMessageTs: cursor.value.lastReceivedMessageTs,
};
cursor.continue();
} else {
try {
func(results);
} catch (e) {
abortWithException(txn, <Error>e);
}
}
};
}
public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: IDBTransaction,
func: (session: ISessionInfo | null) => void,
): void {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.get([deviceKey, sessionId]);
getReq.onsuccess = function (): void {
try {
if (getReq.result) {
func({
session: getReq.result.session,
lastReceivedMessageTs: getReq.result.lastReceivedMessageTs,
});
} else {
func(null);
}
} catch (e) {
abortWithException(txn, <Error>e);
}
};
}
public storeEndToEndSession(
deviceKey: string,
sessionId: string,
sessionInfo: ISessionInfo,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("sessions");
objectStore.put({
deviceKey,
sessionId,
session: sessionInfo.session,
lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs,
});
}
/**
* 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);
}
};
});
if (result.length === 0) {
// No sessions left.
return null;
}
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);
}
});
}
// Inbound group sessions
public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
let session: InboundGroupSessionData | null | boolean = false;
let withheld: IWithheld | null | boolean = false;
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function (): void {
try {
if (getReq.result) {
session = getReq.result.session;
} else {
session = null;
}
if (withheld !== false) {
func(session as InboundGroupSessionData, withheld as IWithheld);
}
} catch (e) {
abortWithException(txn, <Error>e);
}
};
const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
withheldGetReq.onsuccess = function (): void {
try {
if (withheldGetReq.result) {
withheld = withheldGetReq.result.session;
} else {
withheld = null;
}
if (session !== false) {
func(session as InboundGroupSessionData, withheld as IWithheld);
}
} catch (e) {
abortWithException(txn, <Error>e);
}
};
}
public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("inbound_group_sessions");
objectStore.put({
senderCurve25519Key,
sessionId,
session: sessionData,
});
}
/**
* 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 getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-");
getReq.onsuccess = function (): void {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, <Error>e);
}
};
}
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const rooms: Parameters<Parameters<Backend["getEndToEndRooms"]>[1]>[0] = {};
const objectStore = txn.objectStore("rooms");
const getReq = objectStore.openCursor();
getReq.onsuccess = function (): void {
const cursor = getReq.result;
if (cursor) {
rooms[cursor.key as string] = cursor.value;
cursor.continue();
} else {
try {
func(rooms);
} catch (e) {
abortWithException(txn, <Error>e);
}
}
};
}
public async markSessionsNeedingBackup(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.put({
senderCurve25519Key: session.senderKey,
sessionId: session.sessionId,
});
req.onsuccess = resolve;
req.onerror = reject;
});
}),
);
}
public doTxn<T>(
mode: Mode,
stores: string | string[],
func: (txn: IDBTransaction) => T,
log: Logger = logger,
): Promise<T> {
let startTime: number;
let description: string;
if (PROFILE_TRANSACTIONS) {
const txnId = this.nextTxnId++;
startTime = Date.now();
description = `${mode} crypto store transaction ${txnId} in ${stores}`;
log.debug(`Starting ${description}`);
}
const txn = this.db.transaction(stores, mode);
const promise = promiseifyTxn(txn);
const result = func(txn);
if (PROFILE_TRANSACTIONS) {
promise.then(
() => {
const elapsedTime = Date.now() - startTime;
log.debug(`Finished ${description}, took ${elapsedTime} ms`);
},
() => {
const elapsedTime = Date.now() - startTime;
log.error(`Failed ${description}, took ${elapsedTime} ms`);
},
);
}
return promise.then(() => {
return result;
});
}
}
type DbMigration = (db: IDBDatabase) => void;
const DB_MIGRATIONS: DbMigration[] = [
(db): void => {
createDatabase(db);
},
(db): void => {
db.createObjectStore("account");
},
(db): void => {
const sessionsStore = db.createObjectStore("sessions", {
keyPath: ["deviceKey", "sessionId"],
});
sessionsStore.createIndex("deviceKey", "deviceKey");
},
(db): void => {
db.createObjectStore("inbound_group_sessions", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
},
(db): void => {
db.createObjectStore("device_data");
},
(db): void => {
db.createObjectStore("rooms");
},
(db): void => {
db.createObjectStore("sessions_needing_backup", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
},
(db): void => {
db.createObjectStore("inbound_group_sessions_withheld", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
},
(db): void => {
const problemsStore = db.createObjectStore("session_problems", {
keyPath: ["deviceKey", "time"],
});
problemsStore.createIndex("deviceKey", "deviceKey");
db.createObjectStore("notified_error_devices", {
keyPath: ["userId", "deviceId"],
});
},
(db): void => {
db.createObjectStore("shared_history_inbound_group_sessions", {
keyPath: ["roomId"],
});
},
(db): void => {
db.createObjectStore("parked_shared_history", {
keyPath: ["roomId"],
});
},
// Expand as needed.
];
export const VERSION = DB_MIGRATIONS.length;
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
logger.log(`Upgrading IndexedDBCryptoStore from version ${oldVersion} to ${VERSION}`);
DB_MIGRATIONS.forEach((migration, index) => {
if (oldVersion <= index) migration(db);
});
}
function createDatabase(db: IDBDatabase): void {
const outgoingRoomKeyRequestsStore = db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });
// we assume that the RoomKeyRequestBody will have room_id and session_id
// properties, to make the index efficient.
outgoingRoomKeyRequestsStore.createIndex("session", ["requestBody.room_id", "requestBody.session_id"]);
outgoingRoomKeyRequestsStore.createIndex("state", "state");
}
interface IWrappedIDBTransaction extends IDBTransaction {
_mx_abortexception: Error;
}
/*
* Aborts a transaction with a given exception
* The transaction promise will be rejected with this exception.
*/
function abortWithException(txn: IDBTransaction, e: Error): void {
// We cheekily stick our exception onto the transaction object here
// We could alternatively make the thing we pass back to the app
// an object containing the transaction and exception.
(txn as IWrappedIDBTransaction)._mx_abortexception = e;
try {
txn.abort();
} catch {
// sometimes we won't be able to abort the transaction
// (ie. if it's aborted or completed)
}
}
function promiseifyTxn<T>(txn: IDBTransaction): Promise<T | null> {
return new Promise((resolve, reject) => {
txn.oncomplete = (): void => {
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
return;
}
resolve(null);
};
txn.onerror = (event): void => {
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
} else {
logger.log("Error performing indexeddb txn", event);
reject(txn.error);
}
};
txn.onabort = (event): void => {
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
} else {
logger.log("Error performing indexeddb txn", event);
reject(txn.error);
}
};
});
}

View File

@@ -0,0 +1,553 @@
/*
Copyright 2017 - 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.
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, 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 {
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";
/*
* Internal module. indexeddb storage for e2e.
*/
/**
* An implementation of CryptoStore, which is normally backed by an indexeddb,
* but with fallback to MemoryCryptoStore.
*/
export class IndexedDBCryptoStore implements CryptoStore {
public static STORE_ACCOUNT = "account";
public static STORE_SESSIONS = "sessions";
public static STORE_INBOUND_GROUP_SESSIONS = "inbound_group_sessions";
public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = "inbound_group_sessions_withheld";
public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = "shared_history_inbound_group_sessions";
public static STORE_PARKED_SHARED_HISTORY = "parked_shared_history";
public static STORE_DEVICE_DATA = "device_data";
public static STORE_ROOMS = "rooms";
public static STORE_BACKUP = "sessions_needing_backup";
public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
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;
/**
* Create a new IndexedDBCryptoStore
*
* @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);
}
/**
* Ensure the database exists and is up-to-date, or fall back to
* a local storage or in-memory store.
*
* This must be called before the store can be used.
*
* @returns resolves to either an IndexedDBCryptoStoreBackend.Backend,
* or a MemoryCryptoStore
*/
public startup(): Promise<CryptoStore> {
if (this.backendPromise) {
return this.backendPromise;
}
this.backendPromise = new Promise<CryptoStore>((resolve, reject) => {
if (!this.indexedDB) {
reject(new Error("no indexeddb support available"));
return;
}
logger.log(`connecting to indexeddb ${this.dbName}`);
const req = this.indexedDB.open(this.dbName, IndexedDBCryptoStoreBackend.VERSION);
req.onupgradeneeded = (ev): void => {
const db = req.result;
const oldVersion = ev.oldVersion;
IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
};
req.onblocked = (): void => {
logger.log(`can't yet open IndexedDBCryptoStore because it is open elsewhere`);
};
req.onerror = (ev): void => {
logger.log("Error connecting to indexeddb", ev);
reject(req.error);
};
req.onsuccess = (): void => {
const db = req.result;
logger.log(`connected to indexeddb ${this.dbName}`);
resolve(new IndexedDBCryptoStoreBackend.Backend(db));
};
})
.then((backend) => {
// Edge has IndexedDB but doesn't support compund keys which we use fairly extensively.
// Try a dummy query which will fail if the browser doesn't support compund keys, so
// we can fall back to a different backend.
return backend
.doTxn(
"readonly",
[
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
],
(txn) => {
backend.getEndToEndInboundGroupSession("", "", txn, () => {});
},
)
.then(() => backend);
})
.catch((e) => {
if (e.name === "VersionError") {
logger.warn("Crypto DB is too new for us to use!", e);
// don't fall back to a different store: the user has crypto data
// 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}`);
try {
if (!(globalThis.localStorage instanceof Storage)) {
throw new Error("localStorage is not available");
}
return new LocalStorageCryptoStore(globalThis.localStorage);
} catch (e) {
logger.warn(`Unable to open localStorage: falling back to in-memory store: ${e}`);
return new MemoryCryptoStore();
}
})
.then((backend) => {
this.backend = backend;
return backend;
});
return this.backendPromise;
}
/**
* Delete all data from this store.
*
* @returns resolves when the store has been cleared.
*/
public deleteAllData(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!this.indexedDB) {
reject(new Error("no indexeddb support available"));
return;
}
logger.log(`Removing indexeddb instance: ${this.dbName}`);
const req = this.indexedDB.deleteDatabase(this.dbName);
req.onblocked = (): void => {
logger.log(`can't yet delete IndexedDBCryptoStore because it is open elsewhere`);
};
req.onerror = (ev): void => {
logger.log("Error deleting data from indexeddb", ev);
reject(req.error);
};
req.onsuccess = (): void => {
logger.log(`Removed indexeddb instance: ${this.dbName}`);
resolve();
};
}).catch((e) => {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that people can
// still use the app.
logger.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
});
}
/**
* Get data on how much of the libolm to Rust Crypto migration has been done.
*
* Implementation of {@link CryptoStore.getMigrationState}.
*
* @internal
*/
public getMigrationState(): Promise<MigrationState> {
return this.backend!.getMigrationState();
}
/**
* Set data on how much of the libolm to Rust Crypto migration has been done.
*
* Implementation of {@link CryptoStore.setMigrationState}.
*
* @internal
*/
public setMigrationState(migrationState: MigrationState): Promise<void> {
return this.backend!.setMigrationState(migrationState);
}
// Olm Account
/*
* Get the account pickle from the store.
* This requires an active transaction. See doTxn().
*
* @param txn - An active transaction. See doTxn().
* @param func - Called with the account pickle
*/
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
this.backend!.getAccount(txn, func);
}
/**
* Write the account pickle to the store.
* This requires an active transaction. See doTxn().
*
* @param txn - An active transaction. See doTxn().
* @param accountPickle - The new account pickle to store.
*/
public storeAccount(txn: IDBTransaction, accountPickle: string): void {
this.backend!.storeAccount(txn, accountPickle);
}
/**
* Get the public part of the cross-signing keys (eg. self-signing key,
* user signing key).
*
* @param txn - An active transaction. See doTxn().
* @param func - Called with the account keys object:
* `{ key_type: base64 encoded seed }` where key type = user_signing_key_seed or self_signing_key_seed
*/
public getCrossSigningKeys(
txn: IDBTransaction,
func: (keys: Record<string, CrossSigningKeyInfo> | null) => void,
): void {
this.backend!.getCrossSigningKeys(txn, func);
}
/**
* @param txn - An active transaction. See doTxn().
* @param func - Called with the private key
* @param type - A key type
*/
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
this.backend!.getSecretStorePrivateKey(txn, func, type);
}
/**
* Write the cross-signing private keys back to the store
*
* @param txn - An active transaction. See doTxn().
* @param type - The type of cross-signing private key to store
* @param key - keys object as getCrossSigningKeys()
*/
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
type: K,
key: SecretStorePrivateKeys[K],
): void {
this.backend!.storeSecretStorePrivateKey(txn, type, key);
}
// Olm sessions
/**
* Returns the number of end-to-end sessions in the store
* @param txn - An active transaction. See doTxn().
* @param func - Called with the count of sessions
*/
public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
this.backend!.countEndToEndSessions(txn, func);
}
/**
* Retrieve a specific end-to-end session between the logged-in user
* and another device.
* @param deviceKey - The public key of the other device.
* @param sessionId - The ID of the session to retrieve
* @param txn - An active transaction. See doTxn().
* @param func - Called with A map from sessionId
* to session information object with 'session' key being the
* Base64 end-to-end session and lastReceivedMessageTs being the
* timestamp in milliseconds at which the session last received
* a message.
*/
public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: IDBTransaction,
func: (session: ISessionInfo | null) => void,
): void {
this.backend!.getEndToEndSession(deviceKey, sessionId, txn, func);
}
/**
* Retrieve the end-to-end sessions between the logged-in user and another
* device.
* @param deviceKey - The public key of the other device.
* @param txn - An active transaction. See doTxn().
* @param func - Called with A map from sessionId
* to session information object with 'session' key being the
* Base64 end-to-end session and lastReceivedMessageTs being the
* timestamp in milliseconds at which the session last received
* a message.
*/
public getEndToEndSessions(
deviceKey: string,
txn: IDBTransaction,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
this.backend!.getEndToEndSessions(deviceKey, txn, func);
}
/**
* Store a session between the logged-in user and another device
* @param deviceKey - The public key of the other device.
* @param sessionId - The ID for this end-to-end session.
* @param sessionInfo - Session information object
* @param txn - An active transaction. See doTxn().
*/
public storeEndToEndSession(
deviceKey: string,
sessionId: string,
sessionInfo: ISessionInfo,
txn: IDBTransaction,
): void {
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();
}
/**
* Fetch a batch of Olm sessions from the database.
*
* Implementation of {@link CryptoStore.getEndToEndSessionsBatch}.
*
* @internal
*/
public getEndToEndSessionsBatch(): Promise<null | ISessionInfo[]> {
return this.backend!.getEndToEndSessionsBatch();
}
/**
* 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);
}
// Inbound group sessions
/**
* Retrieve the end-to-end inbound group session for a given
* server key and session ID
* @param senderCurve25519Key - The sender's curve 25519 key
* @param sessionId - The ID of the session
* @param txn - An active transaction. See doTxn().
* @param func - Called with A map from sessionId
* to Base64 end-to-end session.
*/
public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
this.backend!.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func);
}
/**
* Writes an end-to-end inbound group session to the store.
* If there already exists an inbound group session with the same
* senderCurve25519Key and sessionID, it will be overwritten.
* @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 storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
this.backend!.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
}
/**
* Fetch a batch of Megolm sessions from the database.
*
* Implementation of {@link CryptoStore.getEndToEndInboundGroupSessionsBatch}.
*
* @internal
*/
public getEndToEndInboundGroupSessionsBatch(): Promise<SessionExtended[] | null> {
return this.backend!.getEndToEndInboundGroupSessionsBatch();
}
/**
* Delete a batch of Megolm sessions from the database.
*
* Implementation of {@link CryptoStore.deleteEndToEndInboundGroupSessionsBatch}.
*
* @internal
*/
public deleteEndToEndInboundGroupSessionsBatch(
sessions: { senderKey: string; sessionId: string }[],
): Promise<void> {
return this.backend!.deleteEndToEndInboundGroupSessionsBatch(sessions);
}
/**
* Get an object of `roomId->roomInfo` for all e2e rooms in the store
* @param txn - An active transaction. See doTxn().
* @param func - Function called with the end-to-end encrypted rooms
*/
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
this.backend!.getEndToEndRooms(txn, func);
}
/**
* Mark 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 marked
*/
public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this.backend!.markSessionsNeedingBackup(sessions, txn);
}
/**
* Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may
* only be called within a callback of either this function or
* one of the store functions operating on the same transaction.
*
* @param mode - 'readwrite' if you need to call setter
* functions with this transaction. Otherwise, 'readonly'.
* @param stores - List IndexedDBCryptoStore.STORE_*
* options representing all types of object that will be
* accessed or written to with this transaction.
* @param func - Function called with the
* transaction object: an opaque object that should be passed
* to store functions.
* @param log - A possibly customised log
* @returns Promise that resolves with the result of the `func`
* when the transaction is complete. If the backend is
* async (ie. the indexeddb backend) any of the callback
* functions throwing an exception will cause this promise to
* 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> {
return this.backend!.doTxn<T>(mode, stores, func as (txn: unknown) => T, log);
}
}

View File

@@ -0,0 +1,408 @@
/*
Copyright 2017 - 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.
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";
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";
/**
* Internal module. Partial localStorage backed storage for e2e.
* This is not a full crypto store, just the in-memory store with
* some things backed by localStorage. It exists because indexedDB
* is broken in Firefox private mode or set to, "will not remember
* history".
*/
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_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/";
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
function keyEndToEndSessions(deviceKey: string): string {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey: string, sessionId: string): string {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndInboundGroupSessionWithheld(senderKey: string, sessionId: string): string {
return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndRoomsPrefix(roomId: string): string {
return KEY_ROOMS_PREFIX + roomId;
}
export class LocalStorageCryptoStore extends MemoryCryptoStore implements CryptoStore {
public static exists(store: Storage): boolean {
const length = store.length;
for (let i = 0; i < length; i++) {
if (store.key(i)?.startsWith(E2E_PREFIX)) {
return true;
}
}
return false;
}
public constructor(private readonly store: Storage) {
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;
}
}
func(count);
}
private _getEndToEndSessions(deviceKey: string): Record<string, ISessionInfo> {
const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
const fixedSessions: Record<string, ISessionInfo> = {};
// fix up any old sessions to be objects rather than just the base64 pickle
for (const [sid, val] of Object.entries(sessions || {})) {
if (typeof val === "string") {
fixedSessions[sid] = {
session: val,
};
} else {
fixedSessions[sid] = val;
}
}
return fixedSessions;
}
public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo) => void,
): void {
const sessions = this._getEndToEndSessions(deviceKey);
func(sessions[sessionId] ?? {});
}
public getEndToEndSessions(
deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
func(this._getEndToEndSessions(deviceKey) ?? {});
}
public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
const sessions = this._getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = sessionInfo;
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;
}
/**
* 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);
}
}
}
// Inbound Group Sessions
public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: unknown,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
func(
getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId)),
getJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId)),
);
}
public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
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;
}
/**
* 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;
}
/**
* 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 getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const result: Record<string, IRoomEncryption> = {};
const prefix = keyEndToEndRoomsPrefix("");
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key?.startsWith(prefix)) {
const roomId = key.slice(prefix.length);
result[roomId] = getJsonItem(this.store, key)!;
}
}
func(result);
}
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
const sessionsNeedingBackup =
getJsonItem<{
[senderKeySessionId: string]: boolean;
}>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
sessionsNeedingBackup[session.senderKey + "/" + session.sessionId] = true;
}
setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup);
return Promise.resolve();
}
/**
* Delete all data from this store.
*
* @returns Promise which resolves when the store has been cleared.
*/
public deleteAllData(): Promise<void> {
this.store.removeItem(KEY_END_TO_END_ACCOUNT);
return Promise.resolve();
}
// Olm account
public getAccount(txn: unknown, func: (accountPickle: string | null) => void): void {
const accountPickle = getJsonItem<string>(this.store, KEY_END_TO_END_ACCOUNT);
func(accountPickle);
}
public storeAccount(txn: unknown, accountPickle: string): void {
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);
func(keys);
}
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
const key = getJsonItem<SecretStorePrivateKeys[K]>(this.store, E2E_PREFIX + `ssss_cache.${type}`);
func(key);
}
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
type: K,
key: SecretStorePrivateKeys[K],
): void {
setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key);
}
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T): Promise<T> {
return Promise.resolve(func(null));
}
}
function getJsonItem<T>(store: Storage, key: string): T | null {
try {
// if the key is absent, store.getItem() returns null, and
// JSON.parse(null) === null, so this returns null.
return JSON.parse(store.getItem(key)!);
} catch (e) {
logger.log("Error: Failed to get key %s: %s", key, (<Error>e).message);
logger.log((<Error>e).stack);
}
return null;
}
function setJsonItem<T>(store: Storage, key: string, val: T): void {
store.setItem(key, JSON.stringify(val));
}

View File

@@ -0,0 +1,326 @@
/*
Copyright 2017 - 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.
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 { safeSet } from "../../utils.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";
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 };
}
/**
* Internal module. in-memory storage for e2e.
*/
export class MemoryCryptoStore implements CryptoStore {
private migrationState: MigrationState = MigrationState.NOT_STARTED;
private account: string | null = null;
private crossSigningKeys: Record<string, CrossSigningKeyInfo> | null = null;
private privateKeys: Partial<SecretStorePrivateKeys> = {};
private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {};
private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {};
private inboundGroupSessionsWithheld: Record<string, IWithheld> = {};
// Opaque device data object
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;
}
/**
* Ensure the database exists and is up-to-date.
*
* This must be called before the store can be used.
*
* @returns resolves to the store.
*/
public async startup(): Promise<CryptoStore> {
// No startup work to do for the memory store.
return this;
}
/**
* Delete all data from this store.
*
* @returns Promise which resolves when the store has been cleared.
*/
public deleteAllData(): Promise<void> {
return Promise.resolve();
}
/**
* 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 this.migrationState;
}
/**
* 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> {
this.migrationState = migrationState;
}
// Olm Account
public getAccount(txn: unknown, func: (accountPickle: string | null) => void): void {
func(this.account);
}
public storeAccount(txn: unknown, accountPickle: string): void {
this.account = accountPickle;
}
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void {
func(this.crossSigningKeys);
}
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
const result = this.privateKeys[type];
func(result || null);
}
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
type: K,
key: SecretStorePrivateKeys[K],
): void {
this.privateKeys[type] = key;
}
// 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);
}
public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo) => void,
): void {
const deviceSessions = this.sessions[deviceKey] || {};
func(deviceSessions[sessionId] || null);
}
public getEndToEndSessions(
deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
func(this.sessions[deviceKey] || {});
}
public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
let deviceSessions = this.sessions[deviceKey];
if (deviceSessions === undefined) {
deviceSessions = {};
this.sessions[deviceKey] = deviceSessions;
}
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;
}
/**
* 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];
}
}
}
// Inbound Group Sessions
public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: unknown,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
const k = encodeSessionKey(senderCurve25519Key, sessionId);
func(this.inboundGroupSessions[k] || null, this.inboundGroupSessionsWithheld[k] || null);
}
public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
const k = encodeSessionKey(senderCurve25519Key, sessionId);
this.inboundGroupSessions[k] = 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;
}
/**
* 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;
}
}
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;
}
/**
* 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];
}
}
// E2E rooms
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
func(this.rooms);
}
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
for (const session of sessions) {
const sessionKey = encodeSessionKey(session.senderKey, session.sessionId);
this.sessionsNeedingBackup[sessionKey] = true;
}
return Promise.resolve();
}
// Session key backups
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
return Promise.resolve(func(null));
}
}

34
node_modules/matrix-js-sdk/src/digest.ts generated vendored Normal file
View File

@@ -0,0 +1,34 @@
/*
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);
}

881
node_modules/matrix-js-sdk/src/embedded.ts generated vendored Normal file
View File

@@ -0,0 +1,881 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
type 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,
} 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";
interface IStateEventRequest {
eventType: string;
stateKey?: string;
}
export interface ICapabilities {
/**
* Event types that this client expects to send.
*/
sendEvent?: string[];
/**
* Event types that this client expects to receive.
*/
receiveEvent?: string[];
/**
* Message types that this client expects to send, or true for all message
* types.
*/
sendMessage?: string[] | true;
/**
* Message types that this client expects to receive, or true for all
* message types.
*/
receiveMessage?: string[] | true;
/**
* Types of state events that this client expects to send.
*/
sendState?: IStateEventRequest[];
/**
* Types of state events that this client expects to receive.
*/
receiveState?: IStateEventRequest[];
/**
* To-device event types that this client expects to send.
*/
sendToDevice?: string[];
/**
* To-device event types that this client expects to receive.
*/
receiveToDevice?: string[];
/**
* Whether this client needs access to TURN servers.
* @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.
* @experimental This class is considered unstable!
*/
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 ||
capabilities.receiveEvent?.length ||
capabilities.sendMessage === true ||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
capabilities.receiveMessage === true ||
(Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length) ||
capabilities.sendState?.length ||
capabilities.receiveState?.length
) {
this.widgetApi.requestCapabilityForRoomTimeline(roomId);
}
capabilities.sendEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToSendEvent(eventType));
capabilities.receiveEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToReceiveEvent(eventType));
if (capabilities.sendMessage === true) {
this.widgetApi.requestCapabilityToSendMessage();
} else if (Array.isArray(capabilities.sendMessage)) {
capabilities.sendMessage.forEach((msgType) => this.widgetApi.requestCapabilityToSendMessage(msgType));
}
if (capabilities.receiveMessage === true) {
this.widgetApi.requestCapabilityToReceiveMessage();
} else if (Array.isArray(capabilities.receiveMessage)) {
capabilities.receiveMessage.forEach((msgType) => this.widgetApi.requestCapabilityToReceiveMessage(msgType));
}
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
this.widgetApi.requestCapabilityToSendState(eventType, stateKey),
);
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
this.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);
}
if (capabilities.turnServers) {
this.widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
}
if (capabilities.rtcTransports) {
this.widgetApi.requestCapability(MatrixCapabilities.MSC4515RtcTransports);
}
}
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();
// Create our own user object artificially (instead of waiting for sync)
// so it's always available, even if the user is not in any rooms etc.
const userId = this.getUserId();
if (userId) {
this.store.storeUser(new User(userId));
}
// Even though we have no access token and cannot sync, the sync class
// still has some valuable helper methods that we make use of, so we
// instantiate it anyways
if (opts.slidingSync) {
this.syncApi = new SlidingSyncSdk(opts.slidingSync, this, opts, this.buildSyncApiOptions());
} 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>));
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,
);
this.setSyncState(SyncState.Syncing);
logger.info("Finished initial sync");
this.matrixRTC.start();
// Watch for TURN servers, if requested
if (this.capabilities.turnServers) this.watchTurnServers();
}
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
}
public async joinRoom(roomIdOrAlias: string): Promise<Room> {
if (roomIdOrAlias === this.roomId) return this.room!;
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 });
let response: ISendEventFromWidgetResponseData;
try {
response = await this.widgetApi
.sendRoomEvent(event.getType(), content, room.roomId, undefined, undefined, stickyDurationMsAsNumber)
.catch(timeoutToConnectionError);
} 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! };
}
public async sendStateEvent(
roomId: string,
eventType: string,
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 };
}
/**
* @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;
}
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());
for (const { userId, deviceId, payload } of batch) {
contentMap.getOrCreate(userId).set(deviceId, payload);
}
await this.widgetApi
.sendToDevice(eventType, false, recursiveMapToObject(contentMap))
.catch(timeoutToConnectionError);
}
/**
* 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);
}
// Overridden since we get TURN servers automatically over the widget API,
// and this method would otherwise complain about missing an access token
public async checkTurnServers(): Promise<boolean> {
return this.turnServers.length > 0;
}
// Overridden since we 'sync' manually without the sync API
public getSyncState(): SyncState | null {
return this.syncState;
}
private setSyncState(state: SyncState): void {
const oldState = this.syncState;
this.syncState = state;
this.emit(ClientEvent.Sync, state, oldState);
}
private async ack(ev: CustomEvent<IWidgetApiRequest>): Promise<void> {
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'",
);
}
}
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()}`);
} else {
const { event_id: eventId, room_id: roomId } = ev.detail.data;
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
}
await this.ack(ev);
};
private onToDevice = async (ev: CustomEvent<ISendToDeviceToWidgetActionRequest>): Promise<void> => {
ev.preventDefault();
const event = new MatrixEvent({
type: ev.detail.data.type,
sender: ev.detail.data.sender,
content: ev.detail.data.content,
});
// 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, {}, "", "");
this.emit(ClientEvent.ToDeviceEvent, event);
this.setSyncState(SyncState.Syncing);
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 => {
servers.return(undefined);
};
this.lifecycle!.signal.addEventListener("abort", onClientStopped);
try {
for await (const server of servers) {
this.turnServers = [
{
urls: server.uris,
username: server.username,
credential: server.password,
},
];
this.emit(ClientEvent.TurnServers, this.turnServers);
logger.log(`Received TURN server: ${server.uris}`);
}
} catch (e) {
logger.warn("Error watching TURN servers", e);
} finally {
this.lifecycle!.signal.removeEventListener("abort", onClientStopped);
}
}
}
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;
}

87
node_modules/matrix-js-sdk/src/errors.ts generated vendored Normal file
View File

@@ -0,0 +1,87 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum InvalidCryptoStoreState {
TooNew = "TOO_NEW",
}
export class InvalidCryptoStoreError extends Error {
public static TOO_NEW = InvalidCryptoStoreState.TooNew;
public constructor(public readonly reason: InvalidCryptoStoreState) {
const message =
`Crypto store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
super(message);
this.name = "InvalidCryptoStoreError";
}
}
export class KeySignatureUploadError extends Error {
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";
}
}

88
node_modules/matrix-js-sdk/src/event-mapper.ts generated vendored Normal file
View File

@@ -0,0 +1,88 @@
/*
Copyright 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.
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 MatrixClient } from "./client.ts";
import { type IEvent, MatrixEvent, MatrixEventEvent } from "./models/event.ts";
import { RelationType } from "./@types/event.ts";
export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
export interface MapperOpts {
// don't re-emit events emitted on an event mapped by this mapper on the client
preventReEmit?: boolean;
// decrypt event proactively
decrypt?: boolean;
}
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
let preventReEmit = Boolean(options.preventReEmit);
const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject: Partial<IEvent>): MatrixEvent {
const room = client.getRoom(plainOldJsObject.room_id);
let event: MatrixEvent | undefined;
// If the event is already known to the room, let's re-use the model rather than duplicating.
// We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour.
if (room && plainOldJsObject.state_key === undefined) {
event = room.findEventById(plainOldJsObject.event_id!);
}
if (!event || event.status) {
event = new MatrixEvent(plainOldJsObject);
} else {
// merge the latest unsigned data from the server
event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned });
// prevent doubling up re-emitters
preventReEmit = true;
}
// if there is a complete edit bundled alongside the event, perform the replacement.
// (prior to MSC3925, events were automatically replaced on the server-side. MSC3925 proposes that that doesn't
// happen automatically but the server does provide us with the whole content of the edit event.)
const bundledEdit = event.getServerAggregatedRelation<Partial<IEvent>>(RelationType.Replace);
if (bundledEdit?.content) {
const replacement = mapper(bundledEdit);
// XXX: it's worth noting that the spec says we should only respect encrypted edits if, once decrypted, the
// replacement has a `m.new_content` property. The problem is that we haven't yet decrypted the replacement
// (it should be happening in the background), so we can't enforce this. Possibly we should for decryption
// to complete, but that sounds a bit racy. For now, we just assume it's ok.
event.makeReplaced(replacement);
}
const thread = room?.findThreadForEvent(event);
if (thread) {
event.setThread(thread);
}
if (event.isEncrypted()) {
if (!preventReEmit) {
client.reEmitter.reEmit(event, [MatrixEventEvent.Decrypted]);
}
if (decrypt) {
client.decryptEventIfNeeded(event);
}
}
if (!preventReEmit) {
client.reEmitter.reEmit(event, [MatrixEventEvent.Replaced, MatrixEventEvent.VisibilityChange]);
room?.reEmitter.reEmit(event, [MatrixEventEvent.BeforeRedaction]);
}
return event;
}
return mapper;
}

View File

@@ -0,0 +1,58 @@
/*
Copyright 2021 - 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.
*/
import { type ExtensibleEventType, type IPartialEvent } from "../@types/extensible_events.ts";
/**
* Represents an Extensible Event in Matrix.
*/
export abstract class ExtensibleEvent<TContent extends object = object> {
protected constructor(public readonly wireFormat: IPartialEvent<TContent>) {}
/**
* Shortcut to wireFormat.content
*/
public get wireContent(): TContent {
return this.wireFormat.content;
}
/**
* Serializes the event into a format which can be used to send the
* event to the room.
* @returns The serialized event.
*/
public abstract serialize(): IPartialEvent<object>;
/**
* Determines if this event is equivalent to the provided event type.
* This is recommended over `instanceof` checks due to issues in the JS
* runtime (and layering of dependencies in some projects).
*
* Implementations should pass this check off to their super classes
* if their own checks fail. Some primary implementations do not extend
* fallback classes given they support the primary type first. Thus,
* those classes may return false if asked about their fallback
* representation.
*
* Note that this only checks primary event types: legacy events, like
* m.room.message, should/will fail this check.
* @param primaryEventType - The (potentially namespaced) event
* type.
* @returns True if this event *could* be represented as the
* given type.
*/
public abstract isEquivalentTo(primaryEventType: ExtensibleEventType): boolean;
}

View File

@@ -0,0 +1,24 @@
/*
Copyright 2022 - 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.
*/
/**
* Thrown when an event is unforgivably unparsable.
*/
export class InvalidEventError extends Error {
public constructor(message: string) {
super(message);
}
}

View File

@@ -0,0 +1,143 @@
/*
Copyright 2022 - 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.
*/
import { ExtensibleEvent } from "./ExtensibleEvent.ts";
import {
type ExtensibleEventType,
type IMessageRendering,
type IPartialEvent,
isEventTypeSame,
M_HTML,
M_MESSAGE,
type ExtensibleAnyMessageEventContent,
M_TEXT,
} from "../@types/extensible_events.ts";
import { isOptionalAString, isProvided } from "./utilities.ts";
import { InvalidEventError } from "./InvalidEventError.ts";
/**
* Represents a message event. Message events are the simplest form of event with
* just text (optionally of different mimetypes, like HTML).
*
* Message events can additionally be an Emote or Notice, though typically those
* are represented as EmoteEvent and NoticeEvent respectively.
*/
export class MessageEvent extends ExtensibleEvent<ExtensibleAnyMessageEventContent> {
/**
* The default text for the event.
*/
public readonly text: string;
/**
* The default HTML for the event, if provided.
*/
public readonly html?: string;
/**
* All the different renderings of the message. Note that this is the same
* format as an m.message body but may contain elements not found directly
* in the event content: this is because this is interpreted based off the
* other information available in the event.
*/
public readonly renderings: IMessageRendering[];
/**
* Creates a new MessageEvent from a pure format. Note that the event is
* *not* parsed here: it will be treated as a literal m.message primary
* typed event.
* @param wireFormat - The event.
*/
public constructor(wireFormat: IPartialEvent<ExtensibleAnyMessageEventContent>) {
super(wireFormat);
const mmessage = M_MESSAGE.findIn(this.wireContent);
const mtext = M_TEXT.findIn<string>(this.wireContent);
const mhtml = M_HTML.findIn<string>(this.wireContent);
if (isProvided(mmessage)) {
if (!Array.isArray(mmessage)) {
throw new InvalidEventError("m.message contents must be an array");
}
const text = mmessage.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain");
const html = mmessage.find((r) => r.mimetype === "text/html");
if (!text) throw new InvalidEventError("m.message is missing a plain text representation");
this.text = text.body;
this.html = html?.body;
this.renderings = mmessage;
} else if (isOptionalAString(mtext)) {
this.text = mtext;
this.html = mhtml ?? undefined;
this.renderings = [{ body: mtext, mimetype: "text/plain" }];
if (this.html) {
this.renderings.push({ body: this.html, mimetype: "text/html" });
}
} else {
throw new InvalidEventError("Missing textual representation for event");
}
}
public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
return isEventTypeSame(primaryEventType, M_MESSAGE);
}
protected serializeMMessageOnly(): ExtensibleAnyMessageEventContent {
let messageRendering: ExtensibleAnyMessageEventContent = {
[M_MESSAGE.name]: this.renderings,
};
// Use the shorthand if it's just a simple text event
if (this.renderings.length === 1) {
const mime = this.renderings[0].mimetype;
if (mime === undefined || mime === "text/plain") {
messageRendering = {
[M_TEXT.name]: this.renderings[0].body,
};
}
}
return messageRendering;
}
public serialize(): IPartialEvent<object> {
return {
type: "m.room.message",
content: {
...this.serializeMMessageOnly(),
body: this.text,
msgtype: "m.text",
format: this.html ? "org.matrix.custom.html" : undefined,
formatted_body: this.html ?? undefined,
},
};
}
/**
* Creates a new MessageEvent from text and HTML.
* @param text - The text.
* @param html - Optional HTML.
* @returns The representative message event.
*/
public static from(text: string, html?: string): MessageEvent {
return new MessageEvent({
type: M_MESSAGE.name,
content: {
[M_TEXT.name]: text,
[M_HTML.name]: html,
},
});
}
}

View File

@@ -0,0 +1,97 @@
/*
Copyright 2022 - 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.
*/
import {
type ExtensibleEventType,
type 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";
/**
* Represents a poll end/closure event.
*/
export class PollEndEvent extends ExtensibleEvent<PollEndEventContent> {
/**
* The poll start event ID referenced by the response.
*/
public readonly pollEventId: string;
/**
* The closing message for the event.
*/
public readonly closingMessage: MessageEvent;
/**
* Creates a new PollEndEvent from a pure format. Note that the event is *not*
* parsed here: it will be treated as a literal m.poll.response primary typed event.
* @param wireFormat - The event.
*/
public constructor(wireFormat: IPartialEvent<PollEndEventContent>) {
super(wireFormat);
const rel = this.wireContent["m.relates_to"];
if (!REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel?.event_id !== "string") {
throw new InvalidEventError("Relationship must be a reference to an event");
}
this.pollEventId = rel.event_id;
this.closingMessage = new MessageEvent(this.wireFormat);
}
public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
return isEventTypeSame(primaryEventType, M_POLL_END);
}
public serialize(): IPartialEvent<object> {
return {
type: M_POLL_END.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: this.pollEventId,
},
[M_POLL_END.name]: {},
...this.closingMessage.serialize().content,
},
};
}
/**
* Creates a new PollEndEvent from a poll event ID.
* @param pollEventId - The poll start event ID.
* @param message - A closing message, typically revealing the top answer.
* @returns The representative poll closure event.
*/
public static from(pollEventId: string, message: string): PollEndEvent {
return new PollEndEvent({
type: M_POLL_END.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: pollEventId,
},
[M_POLL_END.name]: {},
[M_TEXT.name]: message,
},
});
}
}

View File

@@ -0,0 +1,148 @@
/*
Copyright 2022 - 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.
*/
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";
/**
* Represents a poll response event.
*/
export class PollResponseEvent extends ExtensibleEvent<PollResponseEventContent> {
private internalAnswerIds: string[] = [];
private internalSpoiled = false;
/**
* The provided answers for the poll. Note that this may be falsy/unpredictable if
* the `spoiled` property is true.
*/
public get answerIds(): string[] {
return this.internalAnswerIds;
}
/**
* The poll start event ID referenced by the response.
*/
public readonly pollEventId: string;
/**
* Whether the vote is spoiled.
*/
public get spoiled(): boolean {
return this.internalSpoiled;
}
/**
* Creates a new PollResponseEvent from a pure format. Note that the event is *not*
* parsed here: it will be treated as a literal m.poll.response primary typed event.
*
* To validate the response against a poll, call `validateAgainst` after creation.
* @param wireFormat - The event.
*/
public constructor(wireFormat: IPartialEvent<PollResponseEventContent>) {
super(wireFormat);
const rel = this.wireContent["m.relates_to"];
if (!REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel?.event_id !== "string") {
throw new InvalidEventError("Relationship must be a reference to an event");
}
this.pollEventId = rel.event_id;
this.validateAgainst(null);
}
/**
* Validates the poll response using the poll start event as a frame of reference. This
* is used to determine if the vote is spoiled, whether the answers are valid, etc.
* @param poll - The poll start event.
*/
public validateAgainst(poll: PollStartEvent | null): void {
const response = M_POLL_RESPONSE.findIn<PollResponseSubtype>(this.wireContent);
if (!Array.isArray(response?.answers)) {
this.internalSpoiled = true;
this.internalAnswerIds = [];
return;
}
let answers = response?.answers ?? [];
if (answers.some((a) => typeof a !== "string") || answers.length === 0) {
this.internalSpoiled = true;
this.internalAnswerIds = [];
return;
}
if (poll) {
if (answers.some((a) => !poll.answers.some((pa) => pa.id === a))) {
this.internalSpoiled = true;
this.internalAnswerIds = [];
return;
}
answers = answers.slice(0, poll.maxSelections);
}
this.internalAnswerIds = answers;
this.internalSpoiled = false;
}
public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
return isEventTypeSame(primaryEventType, M_POLL_RESPONSE);
}
public serialize(): IPartialEvent<object> {
return {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: this.pollEventId,
},
[M_POLL_RESPONSE.name]: {
answers: this.spoiled ? undefined : this.answerIds,
},
},
};
}
/**
* Creates a new PollResponseEvent from a set of answers. To spoil the vote, pass an empty
* answers array.
* @param answers - The user's answers. Should be valid from a poll's answer IDs.
* @param pollEventId - The poll start event ID.
* @returns The representative poll response event.
*/
public static from(answers: string[], pollEventId: string): PollResponseEvent {
return new PollResponseEvent({
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: pollEventId,
},
[M_POLL_RESPONSE.name]: {
answers: answers,
},
},
});
}
}

View File

@@ -0,0 +1,207 @@
/*
Copyright 2022 - 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.
*/
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 {
type 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";
/**
* Represents a poll answer. Note that this is represented as a subtype and is
* not registered as a parsable event - it is implied for usage exclusively
* within the PollStartEvent parsing.
*/
export class PollAnswerSubevent extends MessageEvent {
/**
* The answer ID.
*/
public readonly id: string;
public constructor(wireFormat: IPartialEvent<PollAnswer>) {
super(wireFormat);
const id = wireFormat.content.id;
if (!id || typeof id !== "string") {
throw new InvalidEventError("Answer ID must be a non-empty string");
}
this.id = id;
}
public serialize(): IPartialEvent<object> {
return {
type: "org.matrix.sdk.poll.answer",
content: {
id: this.id,
...this.serializeMMessageOnly(),
},
};
}
/**
* Creates a new PollAnswerSubevent from ID and text.
* @param id - The answer ID (unique within the poll).
* @param text - The text.
* @returns The representative answer.
*/
public static from(id: string, text: string): PollAnswerSubevent {
return new PollAnswerSubevent({
type: "org.matrix.sdk.poll.answer",
content: {
id: id,
[M_TEXT.name]: text,
},
});
}
}
/**
* Represents a poll start event.
*/
export class PollStartEvent extends ExtensibleEvent<PollStartEventContent> {
/**
* The question being asked, as a MessageEvent node.
*/
public readonly question: MessageEvent;
/**
* The interpreted kind of poll. Note that this will infer a value that is known to the
* SDK rather than verbatim - this means unknown types will be represented as undisclosed
* polls.
*
* To get the raw kind, use rawKind.
*/
public readonly kind: KnownPollKind;
/**
* The true kind as provided by the event sender. Might not be valid.
*/
public readonly rawKind: string;
/**
* The maximum number of selections a user is allowed to make.
*/
public readonly maxSelections: number;
/**
* The possible answers for the poll.
*/
public readonly answers: PollAnswerSubevent[];
/**
* Creates a new PollStartEvent from a pure format. Note that the event is *not*
* parsed here: it will be treated as a literal m.poll.start primary typed event.
* @param wireFormat - The event.
*/
public constructor(wireFormat: IPartialEvent<PollStartEventContent>) {
super(wireFormat);
const poll = M_POLL_START.findIn<PollStartSubtype>(this.wireContent);
if (!poll?.question) {
throw new InvalidEventError("A question is required");
}
this.question = new MessageEvent({ type: "org.matrix.sdk.poll.question", content: poll.question });
this.rawKind = poll.kind;
if (M_POLL_KIND_DISCLOSED.matches(this.rawKind)) {
this.kind = M_POLL_KIND_DISCLOSED;
} else {
this.kind = M_POLL_KIND_UNDISCLOSED; // default & assumed value
}
this.maxSelections =
Number.isFinite(poll.max_selections) && poll.max_selections! > 0 ? poll.max_selections! : 1;
if (!Array.isArray(poll.answers)) {
throw new InvalidEventError("Poll answers must be an array");
}
const answers = poll.answers.slice(0, 20).map(
(a) =>
new PollAnswerSubevent({
type: "org.matrix.sdk.poll.answer",
content: a,
}),
);
if (answers.length <= 0) {
throw new InvalidEventError("No answers available");
}
this.answers = answers;
}
public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
return isEventTypeSame(primaryEventType, M_POLL_START);
}
public serialize(): IPartialEvent<object> {
return {
type: M_POLL_START.name,
content: {
[M_POLL_START.name]: {
question: this.question.serialize().content,
kind: this.rawKind,
max_selections: this.maxSelections,
answers: this.answers.map((a) => a.serialize().content),
},
[M_TEXT.name]: `${this.question.text}\n${this.answers.map((a, i) => `${i + 1}. ${a.text}`).join("\n")}`,
},
};
}
/**
* Creates a new PollStartEvent from question, answers, and metadata.
* @param question - The question to ask.
* @param answers - The answers. Should be unique within each other.
* @param kind - The kind of poll.
* @param maxSelections - The maximum number of selections. Must be 1 or higher.
* @returns The representative poll start event.
*/
public static from(
question: string,
answers: string[],
kind: KnownPollKind | string,
maxSelections = 1,
): PollStartEvent {
return new PollStartEvent({
type: M_POLL_START.name,
content: {
[M_TEXT.name]: question, // unused by parsing
[M_POLL_START.name]: {
question: { [M_TEXT.name]: question },
kind: kind instanceof NamespacedValue ? kind.name : kind,
max_selections: maxSelections,
answers: answers.map((a) => ({ id: makeId(), [M_TEXT.name]: a })),
},
},
});
}
}
const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
function makeId(): string {
return [...Array(16)].map(() => LETTERS.charAt(Math.floor(Math.random() * LETTERS.length))).join("");
}

View File

@@ -0,0 +1,35 @@
/*
Copyright 2021 - 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.
*/
import { type 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 {
return s !== null && s !== undefined;
}
/**
* Determines if the given optional string is a defined string.
* @param s - The input string.
* @returns True if the input is a defined string.
*/
export function isOptionalAString(s: Optional<string>): s is string {
return isProvided(s) && typeof s === "string";
}

85
node_modules/matrix-js-sdk/src/feature.ts generated vendored Normal file
View File

@@ -0,0 +1,85 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type IServerVersions } from "./client.ts";
export enum ServerSupport {
Stable,
Unstable,
Unsupported,
}
export enum Feature {
Thread = "Thread",
ThreadUnreadNotifications = "ThreadUnreadNotifications",
/**
* @deprecated this is now exposed as a capability not a feature
*/
LoginTokenRequest = "LoginTokenRequest",
RelationBasedRedactions = "RelationBasedRedactions",
AccountDataDeletion = "AccountDataDeletion",
RelationsRecursion = "RelationsRecursion",
IntentionalMentions = "IntentionalMentions",
}
type FeatureSupportCondition = {
unstablePrefixes?: string[];
matrixVersion?: string;
};
const featureSupportResolver: Record<string, FeatureSupportCondition> = {
[Feature.Thread]: {
unstablePrefixes: ["org.matrix.msc3440"],
matrixVersion: "v1.3",
},
[Feature.ThreadUnreadNotifications]: {
unstablePrefixes: ["org.matrix.msc3771", "org.matrix.msc3773"],
matrixVersion: "v1.4",
},
[Feature.LoginTokenRequest]: {
unstablePrefixes: ["org.matrix.msc3882"],
},
[Feature.RelationBasedRedactions]: {
unstablePrefixes: ["org.matrix.msc3912"],
},
[Feature.RelationsRecursion]: {
unstablePrefixes: ["org.matrix.msc3981"],
matrixVersion: "v1.10",
},
[Feature.IntentionalMentions]: {
unstablePrefixes: ["org.matrix.msc3952_intentional_mentions"],
matrixVersion: "v1.7",
},
};
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
const supportMap = new Map<Feature, ServerSupport>();
for (const [feature, supportCondition] of Object.entries(featureSupportResolver)) {
const supportMatrixVersion = versions.versions?.includes(supportCondition.matrixVersion || "") ?? false;
const supportUnstablePrefixes =
supportCondition.unstablePrefixes?.every((unstablePrefix) => {
return versions.unstable_features?.[unstablePrefix] === true;
}) ?? false;
if (supportMatrixVersion) {
supportMap.set(feature as Feature, ServerSupport.Stable);
} else if (supportUnstablePrefixes) {
supportMap.set(feature as Feature, ServerSupport.Unstable);
} else {
supportMap.set(feature as Feature, ServerSupport.Unsupported);
}
}
return supportMap;
}

209
node_modules/matrix-js-sdk/src/filter-component.ts generated vendored Normal file
View File

@@ -0,0 +1,209 @@
/*
Copyright 2016 - 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.
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 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";
/**
* Checks if a value matches a given field value, which may be a * terminated
* wildcard pattern.
* @param actualValue - The value to be compared
* @param filterValue - The filter pattern to be compared
* @returns true if the actualValue matches the filterValue
*/
function matchesWildcard(actualValue: string, filterValue: string): boolean {
if (filterValue.endsWith("*")) {
const typePrefix = filterValue.slice(0, -1);
return actualValue.slice(0, typePrefix.length) === typePrefix;
} else {
return actualValue === filterValue;
}
}
export interface IFilterComponent {
"types"?: string[];
"not_types"?: string[];
"rooms"?: string[];
"not_rooms"?: string[];
"senders"?: string[];
"not_senders"?: string[];
"contains_url"?: boolean;
"limit"?: number;
"related_by_senders"?: Array<RelationType | string>;
"related_by_rel_types"?: string[];
// Unstable values
"io.element.relation_senders"?: Array<RelationType | string>;
"io.element.relation_types"?: string[];
}
/**
* FilterComponent is a section of a Filter definition which defines the
* types, rooms, senders filters etc to be applied to a particular type of resource.
* This is all ported over from synapse's Filter object.
*
* N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as
* 'Filters' are referred to as 'FilterCollections'.
*/
export class FilterComponent {
public constructor(
private filterJson: IFilterComponent,
public readonly userId?: string | undefined | null,
) {}
/**
* Checks with the filter component matches the given event
* @param event - event to be checked against the filter
* @returns true if the event matches the filter
*/
public check(event: MatrixEvent): boolean {
const bundledRelationships = event.getUnsigned()?.["m.relations"] || {};
const relations: Array<string | RelationType> = Object.keys(bundledRelationships);
// Relation senders allows in theory a look-up of any senders
// however clients can only know about the current user participation status
// as sending a whole list of participants could be proven problematic in terms
// of performance
// This should be improved when bundled relationships solve that problem
const relationSenders: string[] = [];
if (this.userId && bundledRelationships?.[THREAD_RELATION_TYPE.name]?.current_user_participated) {
relationSenders.push(this.userId);
}
return this.checkFields(
event.getRoomId(),
event.getSender(),
event.getType(),
event.getContent() ? event.getContent().url !== undefined : false,
relations,
relationSenders,
);
}
/**
* 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),
);
}
/**
* Checks whether the filter component matches the given event fields.
* @param roomId - the roomId for the event being checked
* @param sender - the sender of the event being checked
* @param eventType - the type of the event being checked
* @param containsUrl - whether the event contains a content.url field
* @param relationTypes - whether has aggregated relation of the given type
* @param relationSenders - whether one of the relation is sent by the user listed
* @returns true if the event fields match the filter
*/
private checkFields(
roomId: string | undefined,
sender: string | undefined,
eventType: string,
containsUrl: boolean,
relationTypes: Array<RelationType | string>,
relationSenders: string[],
): boolean {
const literalKeys = {
rooms: function (v: string): boolean {
return roomId === v;
},
senders: function (v: string): boolean {
return sender === v;
},
types: function (v: string): boolean {
return matchesWildcard(eventType, v);
},
} 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;
const disallowedValues = this.filterJson[<`not_${keyof typeof literalKeys}`>notName];
if (disallowedValues?.some(matchFunc)) {
return false;
}
const allowedValues = this.filterJson[name as keyof typeof literalKeys];
if (allowedValues && !allowedValues.some(matchFunc)) {
return false;
}
}
const containsUrlFilter = this.filterJson.contains_url;
if (containsUrlFilter !== undefined && containsUrlFilter !== containsUrl) {
return false;
}
const relationTypesFilter = this.filterJson[FILTER_RELATED_BY_REL_TYPES.name];
if (relationTypesFilter !== undefined) {
if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) {
return false;
}
}
const relationSendersFilter = this.filterJson[FILTER_RELATED_BY_SENDERS.name];
if (relationSendersFilter !== undefined) {
if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) {
return false;
}
}
return true;
}
private arrayMatchesFilter(filter: any[], values: any[]): boolean {
return (
values.length > 0 &&
filter.every((value) => {
return values.includes(value);
})
);
}
/**
* Filters a list of events down to those which match this filter component
* @param events - Events to be checked against the filter component
* @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);
}
/**
* Returns the limit field for a given filter component, providing a default of
* 10 if none is otherwise specified. Cargo-culted from Synapse.
* @returns the limit for this filter component.
*/
public limit(): number {
return this.filterJson.limit !== undefined ? this.filterJson.limit : 10;
}
}

268
node_modules/matrix-js-sdk/src/filter.ts generated vendored Normal file
View File

@@ -0,0 +1,268 @@
/*
Copyright 2015 - 2021 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 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");
/**
*/
function setProp(obj: Record<string, any>, keyNesting: string, val: any): void {
const nestedKeys = keyNesting.split(".") as [keyof typeof obj];
let currentObj = obj;
for (let i = 0; i < nestedKeys.length - 1; i++) {
if (!currentObj[nestedKeys[i]]) {
currentObj[nestedKeys[i]] = {};
}
currentObj = currentObj[nestedKeys[i]];
}
currentObj[nestedKeys[nestedKeys.length - 1]] = val;
}
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;
}
export interface IRoomEventFilter extends IFilterComponent {
"lazy_load_members"?: boolean;
"include_redundant_members"?: boolean;
"types"?: Array<EventType | string>;
"related_by_senders"?: Array<RelationType | string>;
"related_by_rel_types"?: string[];
"unread_thread_notifications"?: boolean;
"org.matrix.msc3773.unread_thread_notifications"?: boolean;
// Unstable values
"io.element.relation_senders"?: Array<RelationType | string>;
"io.element.relation_types"?: string[];
}
interface IStateFilter extends IRoomEventFilter {}
interface IRoomFilter {
not_rooms?: string[];
rooms?: string[];
ephemeral?: IRoomEventFilter;
include_leave?: boolean;
state?: IStateFilter;
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[];
}
export class Filter {
public static LAZY_LOADING_MESSAGES_FILTER = {
lazy_load_members: true,
};
/**
* Create a filter from existing data.
*/
public static fromJson(userId: string | undefined | null, filterId: string, jsonObj: IFilterDefinition): Filter {
const filter = new Filter(userId, filterId);
filter.setDefinition(jsonObj);
return filter;
}
private definition: IFilterDefinition = {};
private roomFilter?: FilterComponent;
private roomTimelineFilter?: FilterComponent;
/**
* Construct a new 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,
) {}
/**
* Get the ID of this filter on your homeserver (if known)
* @returns The filter ID
*/
public getFilterId(): string | undefined {
return this.filterId;
}
/**
* Get the JSON body of the filter.
* @returns The filter definition
*/
public getDefinition(): IFilterDefinition {
return this.definition;
}
/**
* Set the JSON body of the filter
* @param definition - The filter definition
*/
public setDefinition(definition: IFilterDefinition): void {
this.definition = definition;
// This is all ported from synapse's FilterCollection()
// definitions look something like:
// {
// "room": {
// "rooms": ["!abcde:example.com"],
// "not_rooms": ["!123456:example.com"],
// "state": {
// "types": ["m.room.*"],
// "not_rooms": ["!726s6s6q:example.com"],
// "lazy_load_members": true,
// },
// "timeline": {
// "limit": 10,
// "types": ["m.room.message"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// "contains_url": true
// },
// "ephemeral": {
// "types": ["m.receipt", "m.typing"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// }
// },
// "presence": {
// "types": ["m.presence"],
// "not_senders": ["@alice:example.com"]
// },
// "event_format": "client",
// "event_fields": ["type", "content", "sender"]
// }
const roomFilterJson = definition.room;
// consider the top level rooms/not_rooms filter
const roomFilterFields: IRoomFilter = {};
if (roomFilterJson) {
if (roomFilterJson.rooms) {
roomFilterFields.rooms = roomFilterJson.rooms;
}
if (roomFilterJson.rooms) {
roomFilterFields.not_rooms = roomFilterJson.not_rooms;
}
}
this.roomFilter = new FilterComponent(roomFilterFields, this.userId);
this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {}, this.userId);
// don't bother porting this from synapse yet:
// this._room_state_filter =
// new FilterComponent(roomFilterJson.state || {});
// this._room_ephemeral_filter =
// new FilterComponent(roomFilterJson.ephemeral || {});
// this._room_account_data_filter =
// new FilterComponent(roomFilterJson.account_data || {});
// this._presence_filter =
// new FilterComponent(definition.presence || {});
// this._account_data_filter =
// new FilterComponent(definition.account_data || {});
}
/**
* Get the room.timeline filter component of the filter
* @returns room timeline filter component
*/
public getRoomTimelineFilterComponent(): FilterComponent | undefined {
return this.roomTimelineFilter;
}
/**
* Filter the list of events based on whether they are allowed in a timeline
* based on this filter
* @param events - the list of events being filtered
* @returns the list of events which match the filter
*/
public filterRoomTimeline(events: MatrixEvent[]): MatrixEvent[] {
if (this.roomFilter) {
events = this.roomFilter.filter(events);
}
if (this.roomTimelineFilter) {
events = this.roomTimelineFilter.filter(events);
}
return events;
}
/**
* Set the max number of events to return for each room's timeline.
* @param limit - The max number of events to return for each room.
*/
public setTimelineLimit(limit: number): void {
setProp(this.definition, "room.timeline.limit", limit);
}
/**
* Enable threads unread notification
*/
public setUnreadThreadNotifications(enabled: boolean): void {
this.definition = {
...this.definition,
room: {
...this.definition?.room,
timeline: {
...this.definition?.room?.timeline,
[UNREAD_THREAD_NOTIFICATIONS.name]: enabled,
},
},
};
}
public setLazyLoadMembers(enabled: boolean): void {
setProp(this.definition, "room.state.lazy_load_members", enabled);
}
/**
* Control whether left rooms should be included in responses.
* @param includeLeave - True to make rooms the user has left appear
* in responses.
*/
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 };
}
}

277
node_modules/matrix-js-sdk/src/http-api/errors.ts generated vendored Normal file
View File

@@ -0,0 +1,277 @@
/*
Copyright 2022 - 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 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";
interface IErrorJson extends Partial<IUsageLimit> {
[key: string]: any; // extensible
errcode?: string;
error?: string;
}
/**
* Construct a generic HTTP error. This is a JavaScript Error with additional information
* 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,
) {
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;
/**
* Construct a Matrix error. This is a JavaScript Error with additional
* 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 url?: string,
public event?: MatrixEvent,
httpHeaders?: Headers,
) {
let message = errorJson.error || "Unknown message";
if (httpStatus) {
message = `[${httpStatus}] ${message}`;
}
if (url) {
message = `${message} (${url})`;
}
super(`MatrixError: ${message}`, httpStatus, httpHeaders);
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;
}
}
/**
* Construct a ConnectionError. This is a JavaScript Error indicating
* that a request failed because of some error with the connection, either
* CORS was not correctly configured on the server, the server didn't response,
* the request timed out, or the internet connection on the client side went down.
*/
export class ConnectionError extends Error {
public constructor(message: string, cause?: Error) {
super(message + (cause ? `: ${cause.message}` : ""));
}
public get name(): string {
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);
}
}
}

377
node_modules/matrix-js-sdk/src/http-api/fetch.ts generated vendored Normal file
View File

@@ -0,0 +1,377 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* This is an internal module. See {@link MatrixHttpApi} for the public class.
*/
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";
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.useAuthorizationHeader = opts.useAuthorizationHeader ?? true;
this.tokenRefresher = new TokenRefresher(opts);
}
public abort(): void {
this.abortController.abort();
this.abortController = new AbortController();
}
public fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof globalThis.fetch> {
if (this.opts.fetchFn) {
return this.opts.fetchFn(resource, options);
}
return globalThis.fetch(resource, options);
}
/**
* Sets the base URL for the identity server
* @param url - The new base url
*/
public setIdBaseUrl(url?: string): void {
this.opts.idBaseUrl = url;
}
public idServerRequest<T extends object = Record<string, unknown>>(
method: Method,
path: string,
params: Record<string, string | string[]> | undefined,
prefix: string,
accessToken?: string,
): Promise<T> {
if (!this.opts.idBaseUrl) {
throw new Error("No identity server base URL set");
}
let queryParams: QueryDict | undefined = undefined;
let body: Record<string, string | string[]> | undefined = undefined;
if (method === Method.Get) {
queryParams = params;
} else {
body = params;
}
const fullUri = this.getUrl(path, queryParams, prefix, this.opts.idBaseUrl);
const opts: IRequestOpts = {
json: true,
headers: {},
};
if (accessToken) {
opts.headers!.Authorization = `Bearer ${accessToken}`;
}
return this.requestOtherUrl(method, fullUri, body, opts);
}
/**
* Perform an authorised request to the homeserver.
* @param method - The HTTP method e.g. "GET".
* @param path - The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
*
* @param queryParams - A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @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.
*
* @returns The parsed response.
* @throws Error if a problem occurred. This includes network problems and Matrix-specific error JSON.
*/
public authedRequest<T>(
method: Method,
path: string,
queryParams: QueryDict = {},
body?: Body,
paramOpts: IRequestOpts = {},
): Promise<T> {
return this.doAuthedRequest<T>(1, method, path, queryParams, body, paramOpts);
}
// 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.useAuthorizationHeader) {
if (!opts.headers) {
opts.headers = {};
}
if (!opts.headers.Authorization) {
opts.headers.Authorization = `Bearer ${requestSnapshot.accessToken}`;
}
if (queryParams.access_token) {
delete queryParams.access_token;
}
} else if (!queryParams.access_token) {
queryParams.access_token = requestSnapshot.accessToken;
}
}
try {
const response = await this.request<T>(method, path, queryParams, body, opts);
return response;
} catch (error) {
if (!(error instanceof MatrixError)) {
throw error;
}
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;
}
}
/**
* Perform a request to the homeserver without any credentials.
* @param method - The HTTP method e.g. "GET".
* @param path - The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
*
* @param queryParams - A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param body - The HTTP JSON body.
*
* @param opts - additional options
*
* @returns The parsed response.
* @throws Error if a problem occurred. This includes network problems and Matrix-specific error JSON.
*/
public request<T = unknown>(
method: Method,
path: string,
queryParams?: QueryDict,
body?: Body,
opts?: IRequestOpts,
): Promise<T> {
const fullUri = this.getUrl(path, queryParams, opts?.prefix, opts?.baseUrl);
return this.requestOtherUrl<T>(method, fullUri, body, opts);
}
/**
* Perform a request to an arbitrary URL.
* @param method - The HTTP method e.g. "GET".
* @param url - The HTTP URL object.
*
* @param body - The HTTP JSON body.
*
* @param opts - additional options
*
* @returns The parsed response.
* @throws 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`");
}
const urlForLogs = this.sanitizeUrlForLogs(url);
this.opts.logger?.debug(`FetchHttpApi: --> ${method} ${urlForLogs}`);
const headers = Object.assign({}, opts.headers || {});
const jsonResponse = !opts.rawResponseBody && opts.json !== false;
if (jsonResponse) {
if (!headers["Accept"]) {
headers["Accept"] = "application/json";
}
}
const timeout = opts.localTimeoutMs ?? this.opts.localTimeoutMs;
const keepAlive = opts.keepAlive ?? false;
const signals = [this.abortController.signal];
if (timeout !== undefined) {
signals.push(timeoutSignal(timeout));
}
if (opts.abortSignal) {
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) {
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 {
res = await this.fetch(url, {
signal,
method,
body: data,
headers,
mode: "cors",
redirect: "follow",
referrer: "",
referrerPolicy: "no-referrer",
cache: cacheMode,
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}]`,
);
} catch (e) {
this.opts.logger?.debug(`FetchHttpApi: <-- ${method} ${urlForLogs} [${Date.now() - start}ms ${e}]`);
if ((<Error>e).name === "AbortError") {
throw e;
}
throw new ConnectionError("fetch failed", <Error>e);
} finally {
cleanup();
}
if (!res.ok) {
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;
}
}
private sanitizeUrlForLogs(url: URL | string): string {
try {
let asUrl: URL;
if (typeof url === "string") {
asUrl = new URL(url);
} else {
asUrl = url;
}
// Remove the values of any URL params that could contain potential secrets
const sanitizedQs = new URLSearchParams();
for (const key of asUrl.searchParams.keys()) {
sanitizedQs.append(key, "xxx");
}
const sanitizedQsString = sanitizedQs.toString();
const sanitizedQsUrlPiece = sanitizedQsString ? `?${sanitizedQsString}` : "";
return asUrl.origin + asUrl.pathname + sanitizedQsUrlPiece;
} catch {
// defensive coding for malformed url
return "??";
}
}
/**
* Form and return a homeserver request URL based on the given path params and prefix.
* @param path - The HTTP path <b>after</b> the supplied prefix e.g. "/createRoom".
* @param queryParams - A dict of query params (these will NOT be urlencoded).
* @param prefix - The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix.
* @param baseUrl - The baseUrl to use e.g. "https://matrix.org", defaulting to this.opts.baseUrl.
* @returns URL
*/
public getUrl(path: string, queryParams?: QueryDict, prefix?: string, baseUrl?: string): URL {
const baseUrlWithFallback = baseUrl ?? this.opts.baseUrl;
const baseUrlWithoutTrailingSlash = baseUrlWithFallback.endsWith("/")
? 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);
}
return url;
}
}

172
node_modules/matrix-js-sdk/src/http-api/index.ts generated vendored Normal file
View File

@@ -0,0 +1,172 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FetchHttpApi } from "./fetch.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";
export * from "./interface.ts";
export * from "./prefix.ts";
export * from "./errors.ts";
export * from "./method.ts";
export * from "./utils.ts";
export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
private uploads: Upload[] = [];
/**
* Upload content to the homeserver
*
* @param file - The object to upload. On a browser, something that
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a Buffer, String or ReadStream.
*
* @param 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
*/
public uploadContent(file: FileType, opts: UploadOpts = {}): Promise<UploadResponse> {
const includeFilename = opts.includeFilename ?? true;
const abortController = opts.abortController ?? new AbortController();
// If the file doesn't have a mime type, use a default since the HS errors if we don't supply one.
const contentType = (opts.type ?? (file as File).type) || "application/octet-stream";
const fileName = opts.name ?? (file as File).name;
const upload = {
loaded: 0,
total: 0,
abortController,
} as Upload;
const uploadResolvers = Promise.withResolvers<UploadResponse>();
if (globalThis.XMLHttpRequest) {
const xhr = new globalThis.XMLHttpRequest();
const timeoutFn = function (): void {
xhr.abort();
uploadResolvers.reject(new Error("Timeout"));
};
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
xhr.onreadystatechange = function (): void {
switch (xhr.readyState) {
case globalThis.XMLHttpRequest.DONE:
callbacks.clearTimeout(timeoutTimer);
try {
if (xhr.status === 0) {
throw new DOMException(xhr.statusText, "AbortError"); // mimic fetch API
}
if (!xhr.responseText) {
throw new Error("No response body.");
}
if (xhr.status >= 400) {
uploadResolvers.reject(parseErrorResponse(xhr, xhr.responseText));
} else {
uploadResolvers.resolve(JSON.parse(xhr.responseText));
}
} catch (err) {
if ((<Error>err).name === "AbortError") {
uploadResolvers.reject(err);
return;
}
uploadResolvers.reject(new ConnectionError("request failed", <Error>err));
}
break;
}
};
xhr.upload.onprogress = (ev: ProgressEvent): void => {
callbacks.clearTimeout(timeoutTimer);
upload.loaded = ev.loaded;
upload.total = ev.total;
timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
opts.progressHandler?.({
loaded: ev.loaded,
total: ev.total,
});
};
const url = this.getUrl("/upload", undefined, MediaPrefix.V3);
if (includeFilename && fileName) {
url.searchParams.set("filename", encodeURIComponent(fileName));
}
if (!this.opts.useAuthorizationHeader && this.opts.accessToken) {
url.searchParams.set("access_token", encodeURIComponent(this.opts.accessToken));
}
xhr.open(Method.Post, url.href);
if (this.opts.useAuthorizationHeader && this.opts.accessToken) {
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
}
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(file);
abortController.signal.addEventListener("abort", () => {
xhr.abort();
});
} else {
const queryParams: QueryDict = {};
if (includeFilename && fileName) {
queryParams.filename = fileName;
}
const headers: Record<string, string> = { "Content-Type": contentType };
this.authedRequest<UploadResponse>(Method.Post, "/upload", queryParams, file, {
prefix: MediaPrefix.V3,
headers,
abortSignal: abortController.signal,
}).then(uploadResolvers.resolve, uploadResolvers.reject);
}
// remove the upload from the list on completion
upload.promise = uploadResolvers.promise.finally(() => {
removeElement(this.uploads, (elem) => elem === upload);
});
abortController.signal.addEventListener("abort", () => {
removeElement(this.uploads, (elem) => elem === upload);
uploadResolvers.reject(new DOMException("Aborted", "AbortError"));
});
this.uploads.push(upload);
return upload.promise;
}
public cancelUpload(promise: Promise<UploadResponse>): boolean {
const upload = this.uploads.find((u) => u.promise === promise);
if (upload) {
upload.abortController.abort();
return true;
}
return false;
}
public getCurrentUploads(): Upload[] {
return this.uploads;
}
}

217
node_modules/matrix-js-sdk/src/http-api/interface.ts generated vendored Normal file
View File

@@ -0,0 +1,217 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MatrixError } from "./errors.ts";
import { type Logger } from "../logger.ts";
import { type QueryDict } from "../utils.ts";
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;
baseUrl: string;
idBaseUrl?: string;
prefix: string;
extraParams?: QueryDict;
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"> {
/**
* map of additional request headers
*/
headers?: Record<string, string>;
abortSignal?: AbortSignal;
/**
* The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*/
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;
// Set to true to prevent the request function from emitting a Session.logged_out event.
// This is intended for use on endpoints where M_UNKNOWN_TOKEN is a valid/notable error response,
// such as with token refreshes.
inhibitLogoutEmit?: boolean;
}
export enum HttpApiEvent {
SessionLoggedOut = "Session.logged_out",
NoConsent = "no_consent",
}
export type HttpApiEventHandlerMap = {
/**
* Fires whenever the login session the JS SDK is using is no
* longer valid and the user must log in again.
* NB. This only fires when action is required from the user, not
* when then login session can be renewed by using a refresh token.
* @example
* ```
* matrixClient.on("Session.logged_out", function(errorObj){
* // show the login screen
* });
* ```
*/
[HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void;
/**
* Fires when the JS SDK receives a M_CONSENT_NOT_GIVEN error in response
* to a HTTP request.
* @example
* ```
* matrixClient.on("no_consent", function(message, contentUri) {
* console.info(message + ' Go to ' + contentUri);
* });
* ```
*/
[HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void;
};
export interface UploadProgress {
loaded: number;
total: number;
}
export interface UploadOpts {
/**
* Name to give the file on the server. Defaults to <tt>file.name</tt>.
*/
name?: string;
/**
* Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
*/
type?: string;
/**
* if false will not send the filename,
* e.g for encrypted file uploads where filename leaks are undesirable.
* Defaults to true.
*/
includeFilename?: boolean;
/**
* Optional. Called when a chunk of
* data has been uploaded, with an object containing the fields `loaded`
* (number of bytes transferred) and `total` (total size, if known).
*/
progressHandler?(progress: UploadProgress): void;
abortController?: AbortController;
}
export interface Upload {
loaded: number;
total: number;
promise: Promise<UploadResponse>;
abortController: AbortController;
}
export interface UploadResponse {
content_uri: string;
}
export type FileType = XMLHttpRequestBodyInit;

25
node_modules/matrix-js-sdk/src/http-api/method.ts generated vendored Normal file
View File

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

48
node_modules/matrix-js-sdk/src/http-api/prefix.ts generated vendored Normal file
View File

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

166
node_modules/matrix-js-sdk/src/http-api/refresh.ts generated vendored Normal file
View File

@@ -0,0 +1,166 @@
/*
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;
}
}
}

212
node_modules/matrix-js-sdk/src/http-api/utils.ts generated vendored Normal file
View File

@@ -0,0 +1,212 @@
/*
Copyright 2022 - 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 { parse as parseContentType, type ContentType } from "content-type";
import { logger } from "../logger.ts";
import { sleep } from "../utils.ts";
import {
ConnectionError,
HTTPError,
MatrixError,
MatrixSafetyError,
MatrixSafetyErrorCode,
safeGetRetryAfterMs,
} from "./errors.ts";
// Ponyfill for https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
export function timeoutSignal(ms: number): AbortSignal {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, ms);
return controller.signal;
}
export function anySignal(signals: AbortSignal[]): {
signal: AbortSignal;
cleanup(this: void): void;
} {
const controller = new AbortController();
function cleanup(): void {
for (const signal of signals) {
signal.removeEventListener("abort", onAbort);
}
}
function onAbort(): void {
controller.abort();
cleanup();
}
for (const signal of signals) {
if (signal.aborted) {
onAbort();
break;
}
signal.addEventListener("abort", onAbort);
}
return {
signal: controller.signal,
cleanup,
};
}
/**
* Attempt to turn an HTTP error response into a Javascript Error.
*
* If it is a JSON response, we will parse it into a MatrixError. Otherwise
* we return a generic Error.
*
* @param response - response object
* @param body - raw body of the response
* @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;
try {
contentType = getResponseContentType(httpHeaders);
} 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,
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`, response.status, httpHeaders);
}
function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest {
return "getResponseHeader" in response;
}
/**
* extract the Content-Type header from response headers, and
* parse it to a `{type, parameters}` object.
*
* returns null if no content-type header could be found.
*
* @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);
}
/**
* Retries a network operation run in a callback.
* @param maxAttempts - maximum attempts to try
* @param callback - callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again.
* @returns the result of the network operation
* @throws {@link ConnectionError} If after maxAttempts the callback still throws ConnectionError
*/
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => Promise<T>): Promise<T> {
let attempts = 0;
let lastConnectionError: ConnectionError | null = null;
while (attempts < maxAttempts) {
try {
if (attempts > 0) {
const timeout = 1000 * Math.pow(2, attempts);
logger.log(`network operation failed ${attempts} times, retrying in ${timeout}ms...`);
await sleep(timeout);
}
return await callback();
} catch (err) {
if (err instanceof ConnectionError) {
attempts += 1;
lastConnectionError = err;
} else {
throw err;
}
}
}
throw lastConnectionError;
}
/**
* 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));
}

25
node_modules/matrix-js-sdk/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1,25 @@
/*
Copyright 2019 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 * as matrixcs from "./matrix.ts";
if (globalThis.__js_sdk_entrypoint) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
}
globalThis.__js_sdk_entrypoint = true;
export * from "./matrix.ts";
export default matrixcs;

50
node_modules/matrix-js-sdk/src/indexeddb-helpers.ts generated vendored Normal file
View File

@@ -0,0 +1,50 @@
/*
Copyright 2019 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.
*/
/**
* Check if an IndexedDB database exists. The only way to do so is to try opening it, so
* we do that and then delete it did not exist before.
*
* @param indexedDB - The `indexedDB` interface
* @param dbName - The database name to test for
* @returns Whether the database exists
*/
export function exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
let exists = true;
const req = indexedDB.open(dbName);
req.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;
};
req.onblocked = (): void => reject(req.error);
req.onsuccess = (): void => {
const db = req.result;
db.close();
if (!exists) {
// 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(exists);
};
req.onerror = (): void => reject(req.error);
});
}

24
node_modules/matrix-js-sdk/src/indexeddb-worker.ts generated vendored Normal file
View File

@@ -0,0 +1,24 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2019 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.
*/
/**
* Separate exports file for the indexeddb web worker, which is designed
* to be used separately
*/
/** The {@link IndexedDBStoreWorker} class. */
export { IndexedDBStoreWorker } from "./store/indexeddb-store-worker.ts";

706
node_modules/matrix-js-sdk/src/interactive-auth.ts generated vendored Normal file
View File

@@ -0,0 +1,706 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 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";
import { type MatrixClient } from "./client.ts";
import { MatrixError } from "./http-api/index.ts";
import { type UserIdentifier } from "./@types/auth.ts";
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
export interface UIAFlow {
stages: Array<AuthType | string>;
}
export interface IInputs {
// An email address. If supplied, a flow using email verification will be chosen.
emailAddress?: string;
// An ISO two letter country code. Gives the country that opts.phoneNumber should be resolved relative to.
phoneCountry?: string;
// A phone number. If supplied, a flow using phone number validation will be chosen.
phoneNumber?: string;
registrationToken?: string;
}
export interface IStageStatus {
emailSid?: string;
errcode?: string;
error?: string;
}
/**
* Data returned in the body of a 401 response from a UIA endpoint.
*
* @see https://spec.matrix.org/v1.6/client-server-api/#user-interactive-api-in-the-rest-api
*/
export interface IAuthData {
/**
* This is a session identifier that the client must pass back to the home server,
* if one is provided, in subsequent attempts to authenticate in the same API call.
*/
session?: string;
/**
* A list of the stages the client has completed successfully
*/
completed?: string[];
/**
* A list of the login flows supported by the server for this API.
*/
flows?: UIAFlow[];
/**
* Contains any information that the client will need to know in order to use a given type of authentication.
* For each login type presented, that type may be present as a key in this dictionary.
* For example, the public part of an OAuth client ID could be given here.
*/
params?: Record<string, Record<string, any>>;
}
export enum AuthType {
Password = "m.login.password",
Recaptcha = "m.login.recaptcha",
Terms = "m.login.terms",
Email = "m.login.email.identity",
Msisdn = "m.login.msisdn",
Sso = "m.login.sso",
SsoUnstable = "org.matrix.login.sso",
Dummy = "m.login.dummy",
RegistrationToken = "m.login.registration_token",
// For backwards compatability with servers that have not yet updated to
// 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",
}
/**
* https://spec.matrix.org/v1.7/client-server-api/#password-based
*/
type PasswordDict = {
type: AuthType.Password;
identifier: UserIdentifier;
password: string;
session: string;
};
/**
* https://spec.matrix.org/v1.7/client-server-api/#google-recaptcha
*/
type RecaptchaDict = {
type: AuthType.Recaptcha;
response: string;
session: string;
};
interface ThreepidCreds {
sid: string;
client_secret: string;
id_server: string;
id_access_token: string;
}
/**
* https://spec.matrix.org/v1.7/client-server-api/#email-based-identity--homeserver
*/
type EmailIdentityDict = {
type: AuthType.Email;
threepid_creds: ThreepidCreds;
session: string;
};
/**
* The parameters which are submitted as the `auth` dict in a UIA request
*
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
*/
export type AuthDict =
| PasswordDict
| RecaptchaDict
| EmailIdentityDict
| { type: Exclude<string, AuthType>; [key: string]: any }
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
| {};
export class NoAuthFlowFoundError extends Error {
public name = "NoAuthFlowFoundError";
public constructor(
m: string,
public readonly required_stages: string[],
public readonly flows: UIAFlow[],
) {
super(m);
}
}
/**
* The type of an application callback to perform the user-interactive bit of UIA.
*
* It is called with a single parameter, `makeRequest`, which is a function which takes the UIA parameters and
* makes the HTTP request. The `authData` parameter in `makeRequest` can be set to null to omit the `auth` field
* from the UIA request.
*
* 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>;
interface IOpts<T> {
/**
* A matrix client to use for the auth process
*/
matrixClient: MatrixClient;
/**
* Error response from the last request. If null, a request will be made with no auth before starting.
*/
authData?: IAuthData;
/**
* Inputs provided by the user and used by different stages of the auto process.
* The inputs provided will affect what flow is chosen.
*/
inputs?: IInputs;
/**
* If resuming an existing interactive auth session, the sessionId of that session.
*/
sessionId?: string;
/**
* If resuming an existing interactive auth session, the client secret for that session
*/
clientSecret?: string;
/**
* If returning from having completed m.login.email.identity auth, the sid for the email verification session.
*/
emailSid?: string;
/**
* If specified, will prefer flows which entirely consist of listed stages.
* These should normally be of type AuthTypes but can be string when supporting custom auth stages.
*
* This can be used to avoid needing the fallback mechanism.
*/
supportedStages?: Array<AuthType | string>;
/**
* Called with the new auth dict to submit the request.
* Also passes a second deprecated arg which is a flag set to true if this request is a background request.
* 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>;
/**
* 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.
* The arguments are: the login type (eg m.login.password); and an object which is either an error or an
* informational object specific to the login type.
* If the 'errcode' key is defined, the object is an error, and has keys:
* errcode: string, the textual error code, eg. M_UNKNOWN
* error: string, human readable string describing the error
*
* The login type specific objects are as follows:
* m.login.email.identity:
* * emailSid: string, the sid of the active email auth session
*/
stateUpdated(this: void, nextStage: AuthType | string, status: IStageStatus): void;
/**
* A function that takes the email address (string), clientSecret (string), attempt number (int) and
* sessionId (string) and calls the relevant requestToken function and returns the promise returned by that
* 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 }>;
/**
* 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
}
/**
* Abstracts the logic used to drive the interactive auth process.
*
* <p>Components implementing an interactive auth flow should instantiate one of
* these, passing in the necessary callbacks to the constructor. They should
* then call attemptAuth, which will return a promise which will resolve or
* reject when the interactive-auth process completes.
*
* <p>Meanwhile, calls will be made to the startAuthStage and doRequest
* callbacks, and information gathered from the user can be submitted with
* submitAuthDict.
*
* @param opts - options object
* @typeParam T - the return type of the request when it is successful
*/
export class InteractiveAuth<T> {
private readonly matrixClient: MatrixClient;
private readonly inputs: IInputs;
private readonly clientSecret: string;
private readonly requestCallback: IOpts<T>["doRequest"];
private readonly busyChangedCallback?: IOpts<T>["busyChanged"];
private readonly stateUpdatedCallback: IOpts<T>["stateUpdated"];
private readonly requestEmailTokenCallback: IOpts<T>["requestEmailToken"];
private readonly supportedStages?: Set<string>;
// The current latest data or error received from the server during the user interactive auth flow.
private data: IAuthData & MatrixError["data"];
private emailSid?: string;
private requestingEmailToken = false;
private attemptAuthDeferred: PromiseWithResolvers<T> | null = null;
private chosenFlow: UIAFlow | null = null;
private currentStage: string | null = null;
private emailAttempt = 1;
// if we are currently trying to submit an auth dict (which includes polling)
// the promise the will resolve/reject when it completes
private submitPromise: Promise<void> | null = null;
public constructor(opts: IOpts<T>) {
this.matrixClient = opts.matrixClient;
this.data = opts.authData || { flows: [] };
this.requestCallback = opts.doRequest;
this.busyChangedCallback = opts.busyChanged;
// startAuthStage included for backwards compat
this.stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
this.requestEmailTokenCallback = opts.requestEmailToken;
this.inputs = opts.inputs || {};
if (opts.sessionId) this.data.session = opts.sessionId;
this.clientSecret = opts.clientSecret || this.matrixClient.generateClientSecret();
this.emailSid = opts.emailSid;
if (opts.supportedStages !== undefined) this.supportedStages = new Set(opts.supportedStages);
}
/**
* begin the authentication process.
*
* @returns which resolves to the response on success,
* or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
* no suitable authentication flow can be found
*/
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();
// pluck the promise out now, as doRequest may clear before we return
const promise = this.attemptAuthDeferred.promise;
// if we have no flows, try a request to acquire the flows
if (!(this.data as IAuthData)?.flows?.length) {
this.busyChangedCallback?.(true);
// use the existing sessionId, if one is present.
const auth = (this.data as IAuthData).session ? { session: (this.data as IAuthData).session } : null;
this.doRequest(auth).finally(() => {
this.busyChangedCallback?.(false);
});
} else {
this.startNextAuthStage();
}
return promise;
}
/**
* Poll to check if the auth session or current stage has been
* completed out-of-band. If so, the attemptAuth promise will
* be resolved.
*/
public async poll(): Promise<void> {
if (!(this.data as IAuthData).session) return;
// likewise don't poll if there is no auth session in progress
if (!this.attemptAuthDeferred) return;
// if we currently have a request in flight, there's no point making
// another just to check what the status is
if (this.submitPromise) return;
let authDict: AuthDict = {};
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.
if (this.emailSid) {
const creds: Record<string, string> = {
sid: this.emailSid,
client_secret: this.clientSecret,
};
const isUrl = this.matrixClient.getIdentityServerUrl();
if (isUrl) {
creds.id_server = new URL(isUrl).host;
}
authDict = {
type: EMAIL_STAGE_TYPE,
threepid_creds: creds,
};
}
}
this.submitAuthDict(authDict, true);
}
/**
* get the auth session ID
*
* @returns session id
*/
public getSessionId(): string | undefined {
return (this.data as IAuthData)?.session;
}
/**
* get the client secret used for validation sessions
* with the identity server.
*
* @returns client secret
*/
public getClientSecret(): string {
return this.clientSecret;
}
/**
* get the server params for a given stage
*
* @param loginType - login type for the stage
* @returns any parameters from the server for this stage
*/
public getStageParams(loginType: string): Record<string, any> | undefined {
return (this.data as IAuthData)?.params?.[loginType];
}
public getChosenFlow(): UIAFlow | null {
return this.chosenFlow;
}
/**
* submit a new auth dict and fire off the request. This will either
* make attemptAuth resolve/reject, or cause the startAuthStage callback
* to be called for a new stage.
*
* @param authData - new auth dict to send to the server. Should
* include a `type` property denoting the login type, as well as any
* other params for that stage.
* @param background - If true, this request failing will not result
* 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> {
if (!this.attemptAuthDeferred) {
throw new Error("submitAuthDict() called before attemptAuth()");
}
if (!background) {
this.busyChangedCallback?.(true);
}
// 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.
while (this.submitPromise) {
try {
await this.submitPromise;
} catch {
// discard any exceptions as we only care when its done,
// not whether it worked or not
}
}
// use the sessionid from the last request, if one is present.
let auth: AuthDict;
if ((this.data as IAuthData)?.session) {
auth = Object.assign(
{
session: (this.data as IAuthData).session,
},
authData,
);
} else {
auth = authData;
}
try {
// NB. the 'background' flag is deprecated by the busyChanged
// callback and is here for backwards compat
this.submitPromise = this.doRequest(auth, background);
await this.submitPromise;
} finally {
this.submitPromise = null;
if (!background) {
this.busyChangedCallback?.(false);
}
}
}
/**
* Gets the sid for the email validation session
* Specific to m.login.email.identity
*
* @returns The sid of the email auth session
*/
public getEmailSid(): string | undefined {
return this.emailSid;
}
/**
* Sets the sid for the email validation session
* This must be set in order to successfully poll for completion
* of the email validation.
* Specific to m.login.email.identity
*
* @param sid - The sid for the email validation session
*/
public setEmailSid(sid: string): void {
this.emailSid = sid;
}
/**
* Requests a new email token and sets the email sid for the validation session
*/
public requestEmailToken = async (): Promise<void> => {
if (!this.requestingEmailToken) {
logger.trace("Requesting email token. Attempt: " + this.emailAttempt);
// If we've picked a flow with email auth, we send the email
// now because we want the request to fail as soon as possible
// if the email address is not valid (ie. already taken or not
// registered, depending on what the operation is).
this.requestingEmailToken = true;
try {
const requestTokenResult = await this.requestEmailTokenCallback(
this.inputs.emailAddress!,
this.clientSecret,
this.emailAttempt++,
(this.data as IAuthData).session!,
);
this.emailSid = requestTokenResult.sid;
logger.trace("Email token request succeeded");
} finally {
this.requestingEmailToken = false;
}
} else {
logger.warn("Could not request email token: Already requesting");
}
};
/**
* Fire off a request, and either resolve the promise, or call
* startAuthStage.
*
* @internal
* @param auth - new auth dict, including session id
* @param background - If true, this request is a background poll, so it
* failing will not result 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.
*/
private async doRequest(auth: AuthDict | null, background = false): Promise<void> {
try {
const result = await this.requestCallback(auth, background);
this.attemptAuthDeferred!.resolve(result);
this.attemptAuthDeferred = null;
} catch (error) {
const matrixError = error instanceof MatrixError ? error : null;
// sometimes UI auth errors don't come with flows
const errorFlows = matrixError?.data?.flows ?? null;
const haveFlows = (this.data as IAuthData)?.flows || Boolean(errorFlows);
if (!matrixError || matrixError.httpStatus !== 401 || !matrixError.data || !haveFlows) {
// doesn't look like an interactive-auth failure.
if (!background) {
this.attemptAuthDeferred?.reject(error);
} else {
// We ignore all failures here (even non-UI auth related ones)
// since we don't want to suddenly fail if the internet connection
// had a blip whilst we were polling
logger.log("Background poll request failed doing UI auth: ignoring", error);
}
}
if (matrixError && !matrixError.data) {
matrixError.data = {};
}
// if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email
// has not yet been validated). This appears to be a Synapse bug, which
// we workaround here.
if (matrixError && !matrixError.data.flows && !matrixError.data.completed && !matrixError.data.session) {
matrixError.data.flows = (this.data as IAuthData).flows;
matrixError.data.completed = (this.data as IAuthData).completed;
matrixError.data.session = (this.data as IAuthData).session;
}
if (matrixError) {
this.data = matrixError.data;
}
try {
this.startNextAuthStage();
} catch (e) {
this.attemptAuthDeferred!.reject(e);
this.attemptAuthDeferred = null;
return;
}
if (!this.emailSid && this.chosenFlow?.stages.includes(AuthType.Email)) {
try {
await this.requestEmailToken();
// NB. promise is not resolved here - at some point, doRequest
// will be called again and if the user has jumped through all
// the hoops correctly, auth will be complete and the request
// will succeed.
// Also, we should expose the fact that this request has compledted
// so clients can know that the email has actually been sent.
} catch (e) {
// we failed to request an email token, so fail the request.
// This could be due to the email already beeing registered
// (or not being registered, depending on what we're trying
// to do) or it could be a network failure. Either way, pass
// the failure up as the user can't complete auth if we can't
// send the email, for whatever reason.
this.attemptAuthDeferred!.reject(e);
this.attemptAuthDeferred = null;
}
}
}
}
/**
* Pick the next stage and call the callback
*
* @internal
* @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
private startNextAuthStage(): void {
const nextStage = this.chooseStage();
if (!nextStage) {
throw new Error("No incomplete flows from the server");
}
this.currentStage = nextStage;
if (nextStage === AuthType.Dummy) {
this.submitAuthDict({
type: "m.login.dummy",
});
return;
}
if (this.data?.errcode || this.data?.error) {
this.stateUpdatedCallback(nextStage, {
errcode: this.data?.errcode || "",
error: this.data?.error || "",
});
return;
}
this.stateUpdatedCallback(nextStage, nextStage === EMAIL_STAGE_TYPE ? { emailSid: this.emailSid } : {});
}
/**
* Pick the next auth stage
*
* @internal
* @returns login type
* @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
private chooseStage(): AuthType | string | undefined {
if (this.chosenFlow === null) {
this.chosenFlow = this.chooseFlow();
}
logger.log("Active flow => %s", JSON.stringify(this.chosenFlow));
const nextStage = this.firstUncompletedStage(this.chosenFlow);
logger.log("Next stage: %s", nextStage);
return nextStage;
}
// Returns a low number for flows we consider best. Counts increase for longer flows and even more so
// for flows which contain stages not listed in `supportedStages`.
private scoreFlow(flow: UIAFlow): number {
let score = flow.stages.length;
if (this.supportedStages !== undefined) {
// Add 10 points to the score for each unsupported stage in the flow.
score += flow.stages.filter((stage) => !this.supportedStages!.has(stage)).length * 10;
}
return score;
}
/**
* Pick one of the flows from the returned list
* If a flow using all of the inputs is found, it will
* be returned, otherwise, null will be returned.
*
* Only flows using all given inputs are chosen because it
* is likely to be surprising if the user provides a
* credential and it is not used. For example, for registration,
* this could result in the email not being used which would leave
* the account with no means to reset a password.
*
* @internal
* @returns flow
* @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
private chooseFlow(): UIAFlow {
const flows = (this.data as IAuthData)?.flows || [];
// we've been given an email or we've already done an email part
const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid);
const haveMsisdn = Boolean(this.inputs.phoneCountry) && Boolean(this.inputs.phoneNumber);
// Flows are not represented in a significant order, so we can choose any we support best
// Sort flows based on how many unsupported stages they contain ascending
flows.sort((a, b) => this.scoreFlow(a) - this.scoreFlow(b));
for (const flow of flows) {
let flowHasEmail = false;
let flowHasMsisdn = false;
for (const stage of flow.stages) {
if (stage === EMAIL_STAGE_TYPE) {
flowHasEmail = true;
} else if (stage == MSISDN_STAGE_TYPE) {
flowHasMsisdn = true;
}
}
if (flowHasEmail == haveEmail && flowHasMsisdn == haveMsisdn) {
return flow;
}
}
const requiredStages: string[] = [];
if (haveEmail) requiredStages.push(EMAIL_STAGE_TYPE);
if (haveMsisdn) requiredStages.push(MSISDN_STAGE_TYPE);
// Throw an error with a fairly generic description, but with more
// information such that the app can give a better one if so desired.
throw new NoAuthFlowFoundError("No appropriate authentication flow found", requiredStages, flows);
}
/**
* Get the first uncompleted stage in the given flow
*
* @internal
* @returns login type
*/
private firstUncompletedStage(flow: UIAFlow): AuthType | string | undefined {
const completed = (this.data as IAuthData)?.completed || [];
return flow.stages.find((stageType) => !completed.includes(stageType));
}
}

277
node_modules/matrix-js-sdk/src/logger.ts generated vendored Normal file
View File

@@ -0,0 +1,277 @@
/*
Copyright 2018 André Jaenisch
Copyright 2019-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 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;
}
// 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
// application.
// An idea would be to control this via an environment variable (on Node.js).
// See https://www.npmjs.com/package/debug to see how this could be implemented
// Part of #332 is introducing a logging library in the first place.
const DEFAULT_NAMESPACE = "matrix";
// because rageshakes in react-sdk hijack the console log, also at module load time,
// initializing the logger here races with the initialization of rageshakes.
// 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) {
return function (this: PrefixedLogger, ...args): void {
if (this.prefix) {
args.unshift(this.prefix);
}
const supportedByConsole =
methodName === "error" ||
methodName === "warn" ||
methodName === "trace" ||
methodName === "info" ||
methodName === "debug";
/* eslint-disable no-console */
if (supportedByConsole) {
return console[methodName](...args);
} else {
return console.log(...args);
}
/* eslint-enable no-console */
};
};
/**
* 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;
/**
* 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);
}
}
/**
* 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;
}
/**
* 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) {}
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);
}
}

175
node_modules/matrix-js-sdk/src/matrix.ts generated vendored Normal file
View File

@@ -0,0 +1,175 @@
/*
Copyright 2015-2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type 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";
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 {
GroupCall,
GroupCallEvent,
GroupCallIntent,
GroupCallState,
GroupCallType,
GroupCallStatsReportEvent,
} from "./webrtc/groupCall.ts";
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";
let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore();
/**
* Configure a different factory to be used for creating crypto stores
*
* @param fac - a function which will return a new `CryptoStore`
*/
export function setCryptoStoreFactory(fac: () => CryptoStore): void {
cryptoStoreFactory = fac;
}
function amendClientOpts(opts: ICreateClientOpts): ICreateClientOpts {
opts.store =
opts.store ??
new MemoryStore({
localStorage: globalThis.localStorage,
});
opts.scheduler = opts.scheduler ?? new MatrixScheduler();
opts.cryptoStore = opts.cryptoStore ?? cryptoStoreFactory();
return opts;
}
/**
* Construct a Matrix Client. Similar to {@link MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
* @param opts - The configuration options for this client. These configuration
* options will be passed directly to {@link MatrixClient}.
*
* @returns A new matrix client.
* @see {@link MatrixClient} for the full list of options for
* `opts`.
*/
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);
}

View File

@@ -0,0 +1,438 @@
/*
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;
}
}
}

View File

@@ -0,0 +1,54 @@
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">;

View File

@@ -0,0 +1,63 @@
/*
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;
}

View File

@@ -0,0 +1,120 @@
/*
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>;
}

View File

@@ -0,0 +1,46 @@
/*
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;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
/*
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)!);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,138 @@
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 }] });
}
}

View File

@@ -0,0 +1,459 @@
/*
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;
}
}

View File

@@ -0,0 +1,197 @@
/*
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 Normal file
View File

@@ -0,0 +1,24 @@
/*
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";

View File

@@ -0,0 +1,27 @@
/*
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")}`);
}
}

Some files were not shown because too many files have changed in this diff Show More