Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use mapped types for account data content #4590

Merged
merged 12 commits into from
Dec 19, 2024
7 changes: 7 additions & 0 deletions spec/unit/crypto/secrets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { decodeBase64 } from "../../../src/base64";
import { CrossSigningKeyInfo } from "../../../src/crypto-api";
import { SecretInfo } from "../../../src/secret-storage.ts";

async function makeTestClient(
userInfo: { userId: string; deviceId: string },
Expand Down Expand Up @@ -68,6 +69,12 @@ function sign<T extends IObject | ICurve25519AuthData>(
};
}

declare module "../../../src/@types/event" {
interface AccountDataEvents {
foo: SecretInfo;
}
}

describe("Secrets", function () {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
Expand Down
6 changes: 6 additions & 0 deletions spec/unit/matrix-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ function convertQueryDictToMap(queryDict?: QueryDict): Map<string, string> {
return new Map(Object.entries(queryDict).map(([k, v]) => [k, String(v)]));
}

declare module "../../src/@types/event" {
interface AccountDataEvents {
"im.vector.test": {};
}
}

type HttpLookup = {
method: string;
path: string;
Expand Down
7 changes: 7 additions & 0 deletions spec/unit/secret-storage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ import {
trimTrailingEquals,
} from "../../src/secret-storage";
import { randomString } from "../../src/randomstring";
import { SecretInfo } from "../../src/secret-storage.ts";

declare module "../../src/@types/event" {
interface AccountDataEvents {
mysecret: SecretInfo;
}
}

describe("ServerSideSecretStorageImpl", function () {
describe(".addKey", function () {
Expand Down
5 changes: 5 additions & 0 deletions src/@types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ export type NonEmptyArray<T> = [T, ...T[]];
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] };

// Based on https://stackoverflow.com/a/57862073
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
export type Assignable<Obj, Item> = {
[Key in keyof Obj]: Obj[Key] extends Item ? Key : never;
}[keyof Obj];
16 changes: 16 additions & 0 deletions src/@types/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ import {
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types.ts";
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls.ts";
import { SessionMembershipData } from "../matrixrtc/CallMembership.ts";
import { LocalNotificationSettings } from "./local_notifications.ts";
import { IPushRules } from "./PushRules.ts";
import { SecretInfo, SecretStorageKeyDescription } from "../secret-storage.ts";
import { POLICIES_ACCOUNT_EVENT_TYPE } from "../models/invites-ignorer-types.ts";

export enum EventType {
// Room state events
Expand Down Expand Up @@ -368,3 +372,15 @@ export interface StateEvents {
// MSC3672
[M_BEACON_INFO.name]: MBeaconInfoEventContent;
}

export interface AccountDataEvents {
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
[EventType.PushRules]: IPushRules;
[EventType.Direct]: { [userId: string]: string[] };
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
[EventType.IgnoredUserList]: { [userId: string]: {} };
"m.secret_storage.default_key": { key: string };
[POLICIES_ACCOUNT_EVENT_TYPE.name]: { [key: string]: any };
"m.identity_server": { base_url: string | null };
[key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings;
[key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription;
"m.megolm_backup.v1": SecretInfo;
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
}
12 changes: 8 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ import {
UpdateDelayedEventAction,
} from "./@types/requests.ts";
import {
AccountDataEvents,
EventType,
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MSC3912_RELATION_BASED_REDACTIONS_PROP,
Expand Down Expand Up @@ -4236,7 +4237,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: an empty object
* @returns Rejects: with an error response.
*/
public setAccountData(eventType: EventType | string, content: IContent): Promise<{}> {
public setAccountData<K extends keyof AccountDataEvents>(
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
eventType: K,
content: AccountDataEvents[K] | {},
): Promise<{}> {
const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId!,
$type: eventType,
Expand Down Expand Up @@ -4287,7 +4291,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}

public async deleteAccountData(eventType: string): Promise<void> {
public async deleteAccountData(eventType: keyof AccountDataEvents): Promise<void> {
const msc3391DeleteAccountDataServerSupport = this.canSupport.get(Feature.AccountDataDeletion);
// if deletion is not supported overwrite with empty content
if (msc3391DeleteAccountDataServerSupport === ServerSupport.Unsupported) {
Expand Down Expand Up @@ -4326,7 +4330,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
userIds.forEach((u) => {
content.ignored_users[u] = {};
});
return this.setAccountData("m.ignored_user_list", content);
return this.setAccountData(EventType.IgnoredUserList, content);
}

/**
Expand Down Expand Up @@ -9264,7 +9268,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
deviceId: string,
notificationSettings: LocalNotificationSettings,
): Promise<{}> {
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}` as const;
return this.setAccountData(key, notificationSettings);
}

Expand Down
33 changes: 15 additions & 18 deletions src/crypto/EncryptionSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { IKeyBackupInfo } from "./keybackup.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { AccountDataClient, SecretStorageKeyDescription } from "../secret-storage.ts";
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../crypto-api/index.ts";
import { AccountDataEvents } from "../@types/event.ts";

interface ICrossSigningKeys {
authUpload: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
Expand Down Expand Up @@ -111,7 +112,10 @@ export class EncryptionSetupBuilder {
userSignatures[deviceId] = signature;
}

public async setAccountData(type: string, content: object): Promise<void> {
public async setAccountData<K extends keyof AccountDataEvents>(
type: K,
content: AccountDataEvents[K],
): Promise<void> {
await this.accountDataClientAdapter.setAccountData(type, content);
}

Expand Down Expand Up @@ -160,7 +164,7 @@ export class EncryptionSetupOperation {
/**
*/
public constructor(
private readonly accountData: Map<string, object>,
private readonly accountData: Map<keyof AccountDataEvents, MatrixEvent>,
private readonly crossSigningKeys?: ICrossSigningKeys,
private readonly keyBackupInfo?: IKeyBackupInfo,
private readonly keySignatures?: KeySignatures,
Expand Down Expand Up @@ -190,7 +194,7 @@ export class EncryptionSetupOperation {
// set account data
if (this.accountData) {
for (const [type, content] of this.accountData) {
await baseApis.setAccountData(type, content);
await baseApis.setAccountData(type, content.getContent());
}
}
// upload first cross-signing signatures with the new key
Expand Down Expand Up @@ -236,7 +240,7 @@ class AccountDataClientAdapter
implements AccountDataClient
{
//
public readonly values = new Map<string, MatrixEvent>();
public readonly values = new Map<keyof AccountDataEvents, MatrixEvent>();

/**
* @param existingValues - existing account data
Expand All @@ -248,33 +252,26 @@ class AccountDataClientAdapter
/**
* @returns the content of the account data
*/
public getAccountDataFromServer<T extends { [k: string]: any }>(type: string): Promise<T | null> {
public getAccountDataFromServer<K extends keyof AccountDataEvents>(type: K): Promise<AccountDataEvents[K] | null> {
return Promise.resolve(this.getAccountData(type));
}

/**
* @returns the content of the account data
*/
public getAccountData<T extends { [k: string]: any }>(type: string): T | null {
const modifiedValue = this.values.get(type);
if (modifiedValue) {
return modifiedValue as unknown as T;
}
const existingValue = this.existingValues.get(type);
if (existingValue) {
return existingValue.getContent<T>();
}
return null;
public getAccountData<K extends keyof AccountDataEvents>(type: K): AccountDataEvents[K] | null {
const event = this.values.get(type) ?? this.existingValues.get(type);
return event?.getContent<AccountDataEvents[K]>() ?? null;
}

public setAccountData(type: string, content: any): Promise<{}> {
public setAccountData<K extends keyof AccountDataEvents>(type: K, content: AccountDataEvents[K]): Promise<{}> {
const event = new MatrixEvent({ type, content });
const lastEvent = this.values.get(type);
this.values.set(type, content);
this.values.set(type, event);
// ensure accountData is emitted on the next tick,
// as SecretStorage listens for it while calling this method
// and it seems to rely on this.
return Promise.resolve().then(() => {
const event = new MatrixEvent({ type, content });
this.emit(ClientEvent.AccountData, event, lastEvent);
return {};
});
Expand Down
8 changes: 4 additions & 4 deletions src/crypto/SecretStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ import {
AccountDataClient,
ServerSideSecretStorage,
ServerSideSecretStorageImpl,
SecretInfoKey,
} from "../secret-storage.ts";
import { ISecretRequest, SecretSharing } from "./SecretSharing.ts";

/* re-exports for backwards compatibility */
export type {
AccountDataClient as IAccountDataClient,
SecretStorageKeyTuple,
SecretStorageKeyObject,
SECRET_STORAGE_ALGORITHM_V1_AES,
Expand Down Expand Up @@ -101,21 +101,21 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> im
/**
* Store an encrypted secret on the server
*/
public store(name: string, secret: string, keys?: string[] | null): Promise<void> {
public store(name: SecretInfoKey, secret: string, keys?: string[] | null): Promise<void> {
return this.storageImpl.store(name, secret, keys);
}

/**
* Get a secret from storage.
*/
public get(name: string): Promise<string | undefined> {
public get(name: SecretInfoKey): Promise<string | undefined> {
return this.storageImpl.get(name);
}

/**
* Check if a secret is stored on the server.
*/
public async isStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
public async isStored(name: SecretInfoKey): Promise<Record<string, SecretStorageKeyDescription> | null> {
return this.storageImpl.isStored(name);
}

Expand Down
7 changes: 4 additions & 3 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
AddSecretStorageKeyOpts,
calculateKeyCheck,
SECRET_STORAGE_ALGORITHM_V1_AES,
SecretInfoKey,
SecretStorageKeyDescription,
SecretStorageKeyObject,
SecretStorageKeyTuple,
Expand Down Expand Up @@ -1194,21 +1195,21 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#store}.
*/
public storeSecret(name: string, secret: string, keys?: string[]): Promise<void> {
public storeSecret(name: SecretInfoKey, secret: string, keys?: string[]): Promise<void> {
return this.secretStorage.store(name, secret, keys);
}

/**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#get}.
*/
public getSecret(name: string): Promise<string | undefined> {
public getSecret(name: SecretInfoKey): Promise<string | undefined> {
return this.secretStorage.get(name);
}

/**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#isStored}.
*/
public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
public isSecretStored(name: SecretInfoKey): Promise<Record<string, SecretStorageKeyDescription> | null> {
return this.secretStorage.isStored(name);
}

Expand Down
58 changes: 58 additions & 0 deletions src/models/invites-ignorer-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
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 "matrix-events-sdk";

/// The event type storing the user's individual policies.
///
/// Exported for testing purposes.
Comment on lines +19 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think /// works for ts doc?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just copied out of invites-ignorer.ts verbatim to avoid an import cycle

export const POLICIES_ACCOUNT_EVENT_TYPE = new UnstableValue("m.policies", "org.matrix.msc3847.policies");

/// The key within the user's individual policies storing the user's ignored invites.
///
/// Exported for testing purposes.
export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue(
"m.ignore.invites",
"org.matrix.msc3847.ignore.invites",
);

/// The types of recommendations understood.
export enum PolicyRecommendation {
Ban = "m.ban",
}

/**
* The various scopes for policies.
*/
export enum PolicyScope {
/**
* The policy deals with an individual user, e.g. reject invites
* from this user.
*/
User = "m.policy.user",

/**
* The policy deals with a room, e.g. reject invites towards
* a specific room.
*/
Room = "m.policy.room",

/**
* The policy deals with a server, e.g. reject invites from
* this server.
*/
Server = "m.policy.server",
}
Loading
Loading