init
This commit is contained in:
29
node_modules/matrix-js-sdk/src/@types/AESEncryptedSecretStoragePayload.ts
generated
vendored
Normal file
29
node_modules/matrix-js-sdk/src/@types/AESEncryptedSecretStoragePayload.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
24
node_modules/matrix-js-sdk/src/@types/IIdentityServerProvider.ts
generated
vendored
Normal file
24
node_modules/matrix-js-sdk/src/@types/IIdentityServerProvider.ts
generated
vendored
Normal 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
203
node_modules/matrix-js-sdk/src/@types/PushRules.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
19
node_modules/matrix-js-sdk/src/@types/another-json.d.ts
generated
vendored
Normal file
19
node_modules/matrix-js-sdk/src/@types/another-json.d.ts
generated
vendored
Normal 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
246
node_modules/matrix-js-sdk/src/@types/auth.ts
generated
vendored
Normal 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
140
node_modules/matrix-js-sdk/src/@types/beacon.ts
generated
vendored
Normal 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
24
node_modules/matrix-js-sdk/src/@types/common.ts
generated
vendored
Normal 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
67
node_modules/matrix-js-sdk/src/@types/crypto.ts
generated
vendored
Normal 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
473
node_modules/matrix-js-sdk/src/@types/event.ts
generated
vendored
Normal 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
119
node_modules/matrix-js-sdk/src/@types/events.ts
generated
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
146
node_modules/matrix-js-sdk/src/@types/extensible_events.ts
generated
vendored
Normal file
146
node_modules/matrix-js-sdk/src/@types/extensible_events.ts
generated
vendored
Normal 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
67
node_modules/matrix-js-sdk/src/@types/global.d.ts
generated
vendored
Normal 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
16
node_modules/matrix-js-sdk/src/@types/json.ts
generated
vendored
Normal 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;
|
||||
19
node_modules/matrix-js-sdk/src/@types/local_notifications.ts
generated
vendored
Normal file
19
node_modules/matrix-js-sdk/src/@types/local_notifications.ts
generated
vendored
Normal 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
92
node_modules/matrix-js-sdk/src/@types/location.ts
generated
vendored
Normal 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;
|
||||
38
node_modules/matrix-js-sdk/src/@types/matrix-sdk-crypto-wasm.d.ts
generated
vendored
Normal file
38
node_modules/matrix-js-sdk/src/@types/matrix-sdk-crypto-wasm.d.ts
generated
vendored
Normal 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
245
node_modules/matrix-js-sdk/src/@types/media.ts
generated
vendored
Normal 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
57
node_modules/matrix-js-sdk/src/@types/membership.ts
generated
vendored
Normal 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
101
node_modules/matrix-js-sdk/src/@types/partials.ts
generated
vendored
Normal 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
120
node_modules/matrix-js-sdk/src/@types/polls.ts
generated
vendored
Normal 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
61
node_modules/matrix-js-sdk/src/@types/read_receipts.ts
generated
vendored
Normal 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
102
node_modules/matrix-js-sdk/src/@types/registration.ts
generated
vendored
Normal 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
340
node_modules/matrix-js-sdk/src/@types/requests.ts
generated
vendored
Normal 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
16
node_modules/matrix-js-sdk/src/@types/retention.ts
generated
vendored
Normal 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
117
node_modules/matrix-js-sdk/src/@types/search.ts
generated
vendored
Normal 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
25
node_modules/matrix-js-sdk/src/@types/signed.ts
generated
vendored
Normal 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
35
node_modules/matrix-js-sdk/src/@types/spaces.ts
generated
vendored
Normal 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
153
node_modules/matrix-js-sdk/src/@types/state_events.ts
generated
vendored
Normal 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
38
node_modules/matrix-js-sdk/src/@types/synapse.ts
generated
vendored
Normal 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
27
node_modules/matrix-js-sdk/src/@types/sync.ts
generated
vendored
Normal 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
29
node_modules/matrix-js-sdk/src/@types/threepids.ts
generated
vendored
Normal 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
69
node_modules/matrix-js-sdk/src/@types/topic.ts
generated
vendored
Normal 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
126
node_modules/matrix-js-sdk/src/@types/type-guards.ts
generated
vendored
Normal 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
24
node_modules/matrix-js-sdk/src/@types/uia.ts
generated
vendored
Normal 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
121
node_modules/matrix-js-sdk/src/NamespacedValue.ts
generated
vendored
Normal 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
93
node_modules/matrix-js-sdk/src/ReEmitter.ts
generated
vendored
Normal 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
154
node_modules/matrix-js-sdk/src/ToDeviceMessageQueue.ts
generated
vendored
Normal 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
505
node_modules/matrix-js-sdk/src/autodiscovery.ts
generated
vendored
Normal 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
86
node_modules/matrix-js-sdk/src/base64.ts
generated
vendored
Normal 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
44
node_modules/matrix-js-sdk/src/browser-index.ts
generated
vendored
Normal 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
102
node_modules/matrix-js-sdk/src/capabilityPoller.ts
generated
vendored
Normal 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
9033
node_modules/matrix-js-sdk/src/client.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
307
node_modules/matrix-js-sdk/src/common-crypto/CryptoBackend.ts
generated
vendored
Normal file
307
node_modules/matrix-js-sdk/src/common-crypto/CryptoBackend.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
4
node_modules/matrix-js-sdk/src/common-crypto/README.md
generated
vendored
Normal file
4
node_modules/matrix-js-sdk/src/common-crypto/README.md
generated
vendored
Normal 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.
|
||||
42
node_modules/matrix-js-sdk/src/common-crypto/key-passphrase.ts
generated
vendored
Normal file
42
node_modules/matrix-js-sdk/src/common-crypto/key-passphrase.ts
generated
vendored
Normal 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
298
node_modules/matrix-js-sdk/src/content-helpers.ts
generated
vendored
Normal 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
122
node_modules/matrix-js-sdk/src/content-repo.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
152
node_modules/matrix-js-sdk/src/crypto-api/CryptoEvent.ts
generated
vendored
Normal file
152
node_modules/matrix-js-sdk/src/crypto-api/CryptoEvent.ts
generated
vendored
Normal 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",
|
||||
}
|
||||
42
node_modules/matrix-js-sdk/src/crypto-api/CryptoEventHandlerMap.ts
generated
vendored
Normal file
42
node_modules/matrix-js-sdk/src/crypto-api/CryptoEventHandlerMap.ts
generated
vendored
Normal 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
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
58
node_modules/matrix-js-sdk/src/crypto-api/key-passphrase.ts
generated
vendored
Normal file
58
node_modules/matrix-js-sdk/src/crypto-api/key-passphrase.ts
generated
vendored
Normal 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
121
node_modules/matrix-js-sdk/src/crypto-api/keybackup.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
69
node_modules/matrix-js-sdk/src/crypto-api/recovery-key.ts
generated
vendored
Normal file
69
node_modules/matrix-js-sdk/src/crypto-api/recovery-key.ts
generated
vendored
Normal 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));
|
||||
}
|
||||
400
node_modules/matrix-js-sdk/src/crypto-api/verification.ts
generated
vendored
Normal file
400
node_modules/matrix-js-sdk/src/crypto-api/verification.ts
generated
vendored
Normal 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
381
node_modules/matrix-js-sdk/src/crypto/store/base.ts
generated
vendored
Normal 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,
|
||||
}
|
||||
656
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts
generated
vendored
Normal file
656
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts
generated
vendored
Normal 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);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
553
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts
generated
vendored
Normal file
553
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts
generated
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
408
node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts
generated
vendored
Normal file
408
node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts
generated
vendored
Normal 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));
|
||||
}
|
||||
326
node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts
generated
vendored
Normal file
326
node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts
generated
vendored
Normal 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
34
node_modules/matrix-js-sdk/src/digest.ts
generated
vendored
Normal 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
881
node_modules/matrix-js-sdk/src/embedded.ts
generated
vendored
Normal 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
87
node_modules/matrix-js-sdk/src/errors.ts
generated
vendored
Normal 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
88
node_modules/matrix-js-sdk/src/event-mapper.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
58
node_modules/matrix-js-sdk/src/extensible_events_v1/ExtensibleEvent.ts
generated
vendored
Normal file
58
node_modules/matrix-js-sdk/src/extensible_events_v1/ExtensibleEvent.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
24
node_modules/matrix-js-sdk/src/extensible_events_v1/InvalidEventError.ts
generated
vendored
Normal file
24
node_modules/matrix-js-sdk/src/extensible_events_v1/InvalidEventError.ts
generated
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
143
node_modules/matrix-js-sdk/src/extensible_events_v1/MessageEvent.ts
generated
vendored
Normal file
143
node_modules/matrix-js-sdk/src/extensible_events_v1/MessageEvent.ts
generated
vendored
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
97
node_modules/matrix-js-sdk/src/extensible_events_v1/PollEndEvent.ts
generated
vendored
Normal file
97
node_modules/matrix-js-sdk/src/extensible_events_v1/PollEndEvent.ts
generated
vendored
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
148
node_modules/matrix-js-sdk/src/extensible_events_v1/PollResponseEvent.ts
generated
vendored
Normal file
148
node_modules/matrix-js-sdk/src/extensible_events_v1/PollResponseEvent.ts
generated
vendored
Normal 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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
207
node_modules/matrix-js-sdk/src/extensible_events_v1/PollStartEvent.ts
generated
vendored
Normal file
207
node_modules/matrix-js-sdk/src/extensible_events_v1/PollStartEvent.ts
generated
vendored
Normal 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("");
|
||||
}
|
||||
35
node_modules/matrix-js-sdk/src/extensible_events_v1/utilities.ts
generated
vendored
Normal file
35
node_modules/matrix-js-sdk/src/extensible_events_v1/utilities.ts
generated
vendored
Normal 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
85
node_modules/matrix-js-sdk/src/feature.ts
generated
vendored
Normal 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
209
node_modules/matrix-js-sdk/src/filter-component.ts
generated
vendored
Normal 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
268
node_modules/matrix-js-sdk/src/filter.ts
generated
vendored
Normal 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
277
node_modules/matrix-js-sdk/src/http-api/errors.ts
generated
vendored
Normal 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
377
node_modules/matrix-js-sdk/src/http-api/fetch.ts
generated
vendored
Normal 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
172
node_modules/matrix-js-sdk/src/http-api/index.ts
generated
vendored
Normal 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
217
node_modules/matrix-js-sdk/src/http-api/interface.ts
generated
vendored
Normal 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
25
node_modules/matrix-js-sdk/src/http-api/method.ts
generated
vendored
Normal 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
48
node_modules/matrix-js-sdk/src/http-api/prefix.ts
generated
vendored
Normal 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
166
node_modules/matrix-js-sdk/src/http-api/refresh.ts
generated
vendored
Normal 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
212
node_modules/matrix-js-sdk/src/http-api/utils.ts
generated
vendored
Normal 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
25
node_modules/matrix-js-sdk/src/index.ts
generated
vendored
Normal 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
50
node_modules/matrix-js-sdk/src/indexeddb-helpers.ts
generated
vendored
Normal 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
24
node_modules/matrix-js-sdk/src/indexeddb-worker.ts
generated
vendored
Normal 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
706
node_modules/matrix-js-sdk/src/interactive-auth.ts
generated
vendored
Normal 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
277
node_modules/matrix-js-sdk/src/logger.ts
generated
vendored
Normal 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
175
node_modules/matrix-js-sdk/src/matrix.ts
generated
vendored
Normal 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);
|
||||
}
|
||||
438
node_modules/matrix-js-sdk/src/matrixrtc/CallMembership.ts
generated
vendored
Normal file
438
node_modules/matrix-js-sdk/src/matrixrtc/CallMembership.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
node_modules/matrix-js-sdk/src/matrixrtc/EncryptionManager.ts
generated
vendored
Normal file
54
node_modules/matrix-js-sdk/src/matrixrtc/EncryptionManager.ts
generated
vendored
Normal 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">;
|
||||
63
node_modules/matrix-js-sdk/src/matrixrtc/IKeyTransport.ts
generated
vendored
Normal file
63
node_modules/matrix-js-sdk/src/matrixrtc/IKeyTransport.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
120
node_modules/matrix-js-sdk/src/matrixrtc/IMembershipManager.ts
generated
vendored
Normal file
120
node_modules/matrix-js-sdk/src/matrixrtc/IMembershipManager.ts
generated
vendored
Normal 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>;
|
||||
}
|
||||
46
node_modules/matrix-js-sdk/src/matrixrtc/LivekitTransport.ts
generated
vendored
Normal file
46
node_modules/matrix-js-sdk/src/matrixrtc/LivekitTransport.ts
generated
vendored
Normal 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;
|
||||
1010
node_modules/matrix-js-sdk/src/matrixrtc/MatrixRTCSession.ts
generated
vendored
Normal file
1010
node_modules/matrix-js-sdk/src/matrixrtc/MatrixRTCSession.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
189
node_modules/matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager.ts
generated
vendored
Normal file
189
node_modules/matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager.ts
generated
vendored
Normal 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)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
1127
node_modules/matrix-js-sdk/src/matrixrtc/MembershipManager.ts
generated
vendored
Normal file
1127
node_modules/matrix-js-sdk/src/matrixrtc/MembershipManager.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
138
node_modules/matrix-js-sdk/src/matrixrtc/MembershipManagerActionScheduler.ts
generated
vendored
Normal file
138
node_modules/matrix-js-sdk/src/matrixrtc/MembershipManagerActionScheduler.ts
generated
vendored
Normal 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 }] });
|
||||
}
|
||||
}
|
||||
459
node_modules/matrix-js-sdk/src/matrixrtc/RTCEncryptionManager.ts
generated
vendored
Normal file
459
node_modules/matrix-js-sdk/src/matrixrtc/RTCEncryptionManager.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
197
node_modules/matrix-js-sdk/src/matrixrtc/ToDeviceKeyTransport.ts
generated
vendored
Normal file
197
node_modules/matrix-js-sdk/src/matrixrtc/ToDeviceKeyTransport.ts
generated
vendored
Normal 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
24
node_modules/matrix-js-sdk/src/matrixrtc/index.ts
generated
vendored
Normal 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";
|
||||
27
node_modules/matrix-js-sdk/src/matrixrtc/membershipData/common.ts
generated
vendored
Normal file
27
node_modules/matrix-js-sdk/src/matrixrtc/membershipData/common.ts
generated
vendored
Normal 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
Reference in New Issue
Block a user