mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
extending SDK; adding automatic bot acc creation
This commit is contained in:
@@ -41,6 +41,7 @@ export async function resolveActionClient(
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: auth.encryption,
|
||||
localTimeoutMs: opts.timeoutMs,
|
||||
|
||||
@@ -5,6 +5,8 @@ import * as credentialsModule from "./credentials.js";
|
||||
import * as sdkModule from "./sdk.js";
|
||||
|
||||
const saveMatrixCredentialsMock = vi.fn();
|
||||
const prepareMatrixRegisterModeMock = vi.fn(async () => null);
|
||||
const finalizeMatrixRegisterConfigAfterSuccessMock = vi.fn(async () => false);
|
||||
|
||||
vi.mock("./credentials.js", () => ({
|
||||
loadMatrixCredentials: vi.fn(() => null),
|
||||
@@ -13,6 +15,13 @@ vi.mock("./credentials.js", () => ({
|
||||
touchMatrixCredentials: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./client/register-mode.js", () => ({
|
||||
prepareMatrixRegisterMode: (...args: unknown[]) => prepareMatrixRegisterModeMock(...args),
|
||||
finalizeMatrixRegisterConfigAfterSuccess: (...args: unknown[]) =>
|
||||
finalizeMatrixRegisterConfigAfterSuccessMock(...args),
|
||||
resetPreparedMatrixRegisterModesForTests: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("resolveMatrixConfig", () => {
|
||||
it("prefers config over env", () => {
|
||||
const cfg = {
|
||||
@@ -96,6 +105,8 @@ describe("resolveMatrixAuth", () => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
saveMatrixCredentialsMock.mockReset();
|
||||
prepareMatrixRegisterModeMock.mockReset();
|
||||
finalizeMatrixRegisterConfigAfterSuccessMock.mockReset();
|
||||
});
|
||||
|
||||
it("uses the hardened client request path for password login and persists deviceId", async () => {
|
||||
@@ -180,6 +191,7 @@ describe("resolveMatrixAuth", () => {
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
type: "m.login.password",
|
||||
device_id: undefined,
|
||||
}),
|
||||
);
|
||||
expect(doRequestSpy).toHaveBeenNthCalledWith(
|
||||
@@ -199,6 +211,94 @@ describe("resolveMatrixAuth", () => {
|
||||
deviceId: "REGDEVICE123",
|
||||
encryption: true,
|
||||
});
|
||||
expect(prepareMatrixRegisterModeMock).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@newbot:example.org",
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
});
|
||||
expect(finalizeMatrixRegisterConfigAfterSuccessMock).toHaveBeenCalledWith({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@newbot:example.org",
|
||||
deviceId: "REGDEVICE123",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores cached credentials when matrix.register=true", async () => {
|
||||
vi.mocked(credentialsModule.loadMatrixCredentials).mockReturnValue({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "cached-token",
|
||||
deviceId: "CACHEDDEVICE",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
vi.mocked(credentialsModule.credentialsMatchConfig).mockReturnValue(true);
|
||||
|
||||
const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest").mockResolvedValue({
|
||||
access_token: "tok-123",
|
||||
user_id: "@bot:example.org",
|
||||
device_id: "DEVICE123",
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
password: "secret",
|
||||
register: true,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
const auth = await resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv });
|
||||
|
||||
expect(doRequestSpy).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
"/_matrix/client/v3/login",
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
type: "m.login.password",
|
||||
}),
|
||||
);
|
||||
expect(auth.accessToken).toBe("tok-123");
|
||||
expect(prepareMatrixRegisterModeMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("requires matrix.password when matrix.register=true", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
register: true,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
await expect(resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv })).rejects.toThrow(
|
||||
"Matrix password is required when matrix.register=true",
|
||||
);
|
||||
expect(prepareMatrixRegisterModeMock).not.toHaveBeenCalled();
|
||||
expect(finalizeMatrixRegisterConfigAfterSuccessMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires matrix.userId when matrix.register=true", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
password: "secret",
|
||||
register: true,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
await expect(resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv })).rejects.toThrow(
|
||||
"Matrix userId is required when matrix.register=true",
|
||||
);
|
||||
expect(prepareMatrixRegisterModeMock).not.toHaveBeenCalled();
|
||||
expect(finalizeMatrixRegisterConfigAfterSuccessMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to config deviceId when cached credentials are missing it", async () => {
|
||||
|
||||
@@ -3,6 +3,10 @@ import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
import { MatrixClient } from "../sdk.js";
|
||||
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
||||
import {
|
||||
finalizeMatrixRegisterConfigAfterSuccess,
|
||||
prepareMatrixRegisterMode,
|
||||
} from "./register-mode.js";
|
||||
|
||||
function clean(value?: string): string {
|
||||
return value?.trim() ?? "";
|
||||
@@ -42,6 +46,7 @@ async function registerMatrixPasswordAccount(params: {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
password: string;
|
||||
deviceId?: string;
|
||||
deviceName?: string;
|
||||
}): Promise<{
|
||||
access_token?: string;
|
||||
@@ -53,6 +58,7 @@ async function registerMatrixPasswordAccount(params: {
|
||||
username: resolveMatrixLocalpart(params.userId),
|
||||
password: params.password,
|
||||
inhibit_login: false,
|
||||
device_id: params.deviceId,
|
||||
initial_device_display_name: params.deviceName ?? "OpenClaw Gateway",
|
||||
};
|
||||
|
||||
@@ -128,6 +134,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
const cfg = params?.cfg ?? (getMatrixRuntime().config.loadConfig() as CoreConfig);
|
||||
const env = params?.env ?? process.env;
|
||||
const resolved = resolveMatrixConfig(cfg, env);
|
||||
const registerFromConfig = cfg.channels?.matrix?.register === true;
|
||||
if (!resolved.homeserver) {
|
||||
throw new Error("Matrix homeserver is required (matrix.homeserver)");
|
||||
}
|
||||
@@ -149,8 +156,23 @@ export async function resolveMatrixAuth(params?: {
|
||||
? cached
|
||||
: null;
|
||||
|
||||
if (registerFromConfig) {
|
||||
if (!resolved.userId) {
|
||||
throw new Error("Matrix userId is required when matrix.register=true");
|
||||
}
|
||||
if (!resolved.password) {
|
||||
throw new Error("Matrix password is required when matrix.register=true");
|
||||
}
|
||||
await prepareMatrixRegisterMode({
|
||||
cfg,
|
||||
homeserver: resolved.homeserver,
|
||||
userId: resolved.userId,
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
// If we have an access token, we can fetch userId via whoami if not provided
|
||||
if (resolved.accessToken) {
|
||||
if (resolved.accessToken && !registerFromConfig) {
|
||||
let userId = resolved.userId;
|
||||
const hasMatchingCachedToken = cachedCredentials?.accessToken === resolved.accessToken;
|
||||
let knownDeviceId = hasMatchingCachedToken
|
||||
@@ -196,6 +218,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
homeserver: resolved.homeserver,
|
||||
userId,
|
||||
accessToken: resolved.accessToken,
|
||||
password: resolved.password,
|
||||
deviceId: knownDeviceId,
|
||||
deviceName: resolved.deviceName,
|
||||
initialSyncLimit: resolved.initialSyncLimit,
|
||||
@@ -203,12 +226,13 @@ export async function resolveMatrixAuth(params?: {
|
||||
};
|
||||
}
|
||||
|
||||
if (cachedCredentials) {
|
||||
if (cachedCredentials && !registerFromConfig) {
|
||||
touchMatrixCredentials(env);
|
||||
return {
|
||||
homeserver: cachedCredentials.homeserver,
|
||||
userId: cachedCredentials.userId,
|
||||
accessToken: cachedCredentials.accessToken,
|
||||
password: resolved.password,
|
||||
deviceId: cachedCredentials.deviceId || resolved.deviceId,
|
||||
deviceName: resolved.deviceName,
|
||||
initialSyncLimit: resolved.initialSyncLimit,
|
||||
@@ -239,6 +263,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
type: "m.login.password",
|
||||
identifier: { type: "m.id.user", user: resolved.userId },
|
||||
password: resolved.password,
|
||||
device_id: resolved.deviceId,
|
||||
initial_device_display_name: resolved.deviceName ?? "OpenClaw Gateway",
|
||||
})) as {
|
||||
access_token?: string;
|
||||
@@ -254,6 +279,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
homeserver: resolved.homeserver,
|
||||
userId: resolved.userId,
|
||||
password: resolved.password,
|
||||
deviceId: resolved.deviceId,
|
||||
deviceName: resolved.deviceName,
|
||||
});
|
||||
} catch (registerErr) {
|
||||
@@ -275,7 +301,8 @@ export async function resolveMatrixAuth(params?: {
|
||||
homeserver: resolved.homeserver,
|
||||
userId: login.user_id ?? resolved.userId,
|
||||
accessToken,
|
||||
deviceId: login.device_id,
|
||||
password: resolved.password,
|
||||
deviceId: login.device_id ?? resolved.deviceId,
|
||||
deviceName: resolved.deviceName,
|
||||
initialSyncLimit: resolved.initialSyncLimit,
|
||||
encryption: resolved.encryption,
|
||||
@@ -285,8 +312,16 @@ export async function resolveMatrixAuth(params?: {
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
deviceId: login.device_id,
|
||||
deviceId: auth.deviceId,
|
||||
});
|
||||
|
||||
if (registerFromConfig) {
|
||||
await finalizeMatrixRegisterConfigAfterSuccess({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
deviceId: auth.deviceId,
|
||||
});
|
||||
}
|
||||
|
||||
return auth;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export async function createMatrixClient(params: {
|
||||
homeserver: string;
|
||||
userId?: string;
|
||||
accessToken: string;
|
||||
password?: string;
|
||||
deviceId?: string;
|
||||
encryption?: boolean;
|
||||
localTimeoutMs?: number;
|
||||
@@ -43,6 +44,7 @@ export async function createMatrixClient(params: {
|
||||
|
||||
return new MatrixClient(params.homeserver, params.accessToken, undefined, undefined, {
|
||||
userId: matrixClientUserId,
|
||||
password: params.password,
|
||||
deviceId: params.deviceId,
|
||||
encryption: params.encryption,
|
||||
localTimeoutMs: params.localTimeoutMs,
|
||||
|
||||
97
extensions/matrix/src/matrix/client/register-mode.test.ts
Normal file
97
extensions/matrix/src/matrix/client/register-mode.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import * as runtimeModule from "../../runtime.js";
|
||||
import {
|
||||
finalizeMatrixRegisterConfigAfterSuccess,
|
||||
prepareMatrixRegisterMode,
|
||||
resetPreparedMatrixRegisterModesForTests,
|
||||
} from "./register-mode.js";
|
||||
|
||||
describe("matrix register mode helpers", () => {
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
resetPreparedMatrixRegisterModesForTests();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("moves existing matrix state into a .bak snapshot before fresh registration", async () => {
|
||||
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-register-mode-"));
|
||||
tempDirs.push(stateDir);
|
||||
const credentialsDir = path.join(stateDir, "credentials", "matrix");
|
||||
const accountsDir = path.join(credentialsDir, "accounts");
|
||||
fs.mkdirSync(accountsDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(credentialsDir, "credentials.json"), '{"accessToken":"old"}\n');
|
||||
fs.writeFileSync(path.join(accountsDir, "dummy.txt"), "old-state\n");
|
||||
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
userId: "@pinguini:matrix.gumadeiras.com",
|
||||
register: true,
|
||||
encryption: true,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
const backupDir = await prepareMatrixRegisterMode({
|
||||
cfg,
|
||||
homeserver: "https://matrix.gumadeiras.com",
|
||||
userId: "@pinguini:matrix.gumadeiras.com",
|
||||
env: { OPENCLAW_STATE_DIR: stateDir } as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(backupDir).toBeTruthy();
|
||||
expect(fs.existsSync(path.join(credentialsDir, "credentials.json"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(credentialsDir, "accounts"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(backupDir as string, "credentials.json"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(backupDir as string, "accounts", "dummy.txt"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(backupDir as string, "matrix-config.json"))).toBe(true);
|
||||
});
|
||||
|
||||
it("updates matrix config after successful register mode auth", async () => {
|
||||
const writeConfigFile = vi.fn(async () => {});
|
||||
vi.spyOn(runtimeModule, "getMatrixRuntime").mockReturnValue({
|
||||
config: {
|
||||
loadConfig: () =>
|
||||
({
|
||||
channels: {
|
||||
matrix: {
|
||||
register: true,
|
||||
accessToken: "stale-token",
|
||||
userId: "@pinguini:matrix.gumadeiras.com",
|
||||
},
|
||||
},
|
||||
}) as CoreConfig,
|
||||
writeConfigFile,
|
||||
},
|
||||
} as never);
|
||||
|
||||
const updated = await finalizeMatrixRegisterConfigAfterSuccess({
|
||||
homeserver: "https://matrix.gumadeiras.com",
|
||||
userId: "@pinguini:matrix.gumadeiras.com",
|
||||
deviceId: "DEVICE123",
|
||||
});
|
||||
expect(updated).toBe(true);
|
||||
expect(writeConfigFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channels: expect.objectContaining({
|
||||
matrix: expect.objectContaining({
|
||||
register: false,
|
||||
homeserver: "https://matrix.gumadeiras.com",
|
||||
userId: "@pinguini:matrix.gumadeiras.com",
|
||||
deviceId: "DEVICE123",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const written = writeConfigFile.mock.calls[0]?.[0] as CoreConfig;
|
||||
expect(written.channels?.matrix?.accessToken).toBeUndefined();
|
||||
});
|
||||
});
|
||||
125
extensions/matrix/src/matrix/client/register-mode.ts
Normal file
125
extensions/matrix/src/matrix/client/register-mode.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
import { resolveMatrixCredentialsDir } from "../credentials.js";
|
||||
|
||||
const preparedRegisterKeys = new Set<string>();
|
||||
|
||||
function resolveStateDirFromEnv(env: NodeJS.ProcessEnv): string {
|
||||
try {
|
||||
return getMatrixRuntime().state.resolveStateDir(env, os.homedir);
|
||||
} catch {
|
||||
// fall through to deterministic fallback for tests/early init
|
||||
}
|
||||
const override = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
|
||||
if (override) {
|
||||
if (override.startsWith("~")) {
|
||||
const expanded = override.replace(/^~(?=$|[\\/])/, os.homedir());
|
||||
return path.resolve(expanded);
|
||||
}
|
||||
return path.resolve(override);
|
||||
}
|
||||
return path.join(os.homedir(), ".openclaw");
|
||||
}
|
||||
|
||||
function buildRegisterKey(params: { homeserver: string; userId: string }): string {
|
||||
return `${params.homeserver.trim().toLowerCase()}|${params.userId.trim().toLowerCase()}`;
|
||||
}
|
||||
|
||||
function buildBackupDirName(now = new Date()): string {
|
||||
const ts = now.toISOString().replace(/[:.]/g, "-");
|
||||
const suffix = Math.random().toString(16).slice(2, 8);
|
||||
return `${ts}-${suffix}`;
|
||||
}
|
||||
|
||||
export async function prepareMatrixRegisterMode(params: {
|
||||
cfg: CoreConfig;
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<string | null> {
|
||||
const env = params.env ?? process.env;
|
||||
const registerKey = buildRegisterKey({
|
||||
homeserver: params.homeserver,
|
||||
userId: params.userId,
|
||||
});
|
||||
if (preparedRegisterKeys.has(registerKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stateDir = resolveStateDirFromEnv(env);
|
||||
const credentialsDir = resolveMatrixCredentialsDir(env, stateDir);
|
||||
if (!fs.existsSync(credentialsDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(credentialsDir).filter((name) => name !== ".bak");
|
||||
if (entries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const backupRoot = path.join(credentialsDir, ".bak");
|
||||
fs.mkdirSync(backupRoot, { recursive: true });
|
||||
const backupDir = path.join(backupRoot, buildBackupDirName());
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
|
||||
const matrixConfig = params.cfg.channels?.matrix ?? {};
|
||||
fs.writeFileSync(
|
||||
path.join(backupDir, "matrix-config.json"),
|
||||
JSON.stringify(matrixConfig, null, 2).trimEnd().concat("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
for (const entry of entries) {
|
||||
fs.renameSync(path.join(credentialsDir, entry), path.join(backupDir, entry));
|
||||
}
|
||||
|
||||
preparedRegisterKeys.add(registerKey);
|
||||
return backupDir;
|
||||
}
|
||||
|
||||
export async function finalizeMatrixRegisterConfigAfterSuccess(params: {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
deviceId?: string;
|
||||
}): Promise<boolean> {
|
||||
let runtime: ReturnType<typeof getMatrixRuntime> | null = null;
|
||||
try {
|
||||
runtime = getMatrixRuntime();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cfg = runtime.config.loadConfig() as CoreConfig;
|
||||
if (cfg.channels?.matrix?.register !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matrixCfg = cfg.channels?.matrix ?? {};
|
||||
const nextMatrix: Record<string, unknown> = {
|
||||
...matrixCfg,
|
||||
register: false,
|
||||
homeserver: params.homeserver,
|
||||
userId: params.userId,
|
||||
...(params.deviceId?.trim() ? { deviceId: params.deviceId.trim() } : {}),
|
||||
};
|
||||
// Registration mode should continue relying on password + cached credentials, not stale inline token.
|
||||
delete nextMatrix.accessToken;
|
||||
|
||||
const next: CoreConfig = {
|
||||
...cfg,
|
||||
channels: {
|
||||
...(cfg.channels ?? {}),
|
||||
matrix: nextMatrix as CoreConfig["channels"]["matrix"],
|
||||
},
|
||||
};
|
||||
|
||||
await runtime.config.writeConfigFile(next as never);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resetPreparedMatrixRegisterModesForTests(): void {
|
||||
preparedRegisterKeys.clear();
|
||||
}
|
||||
@@ -36,6 +36,7 @@ async function createSharedMatrixClient(params: {
|
||||
homeserver: params.auth.homeserver,
|
||||
userId: params.auth.userId,
|
||||
accessToken: params.auth.accessToken,
|
||||
password: params.auth.password,
|
||||
deviceId: params.auth.deviceId,
|
||||
encryption: params.auth.encryption,
|
||||
localTimeoutMs: params.timeoutMs,
|
||||
|
||||
@@ -21,6 +21,7 @@ export type MatrixAuth = {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
password?: string;
|
||||
deviceId?: string;
|
||||
deviceName?: string;
|
||||
initialSyncLimit?: number;
|
||||
|
||||
@@ -104,6 +104,7 @@ type MatrixJsClientStub = EventEmitter & {
|
||||
fetchRoomEvent: ReturnType<typeof vi.fn>;
|
||||
sendTyping: ReturnType<typeof vi.fn>;
|
||||
getRoom: ReturnType<typeof vi.fn>;
|
||||
getRooms: ReturnType<typeof vi.fn>;
|
||||
getCrypto: ReturnType<typeof vi.fn>;
|
||||
decryptEventIfNeeded: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
@@ -132,6 +133,7 @@ function createMatrixJsClientStub(): MatrixJsClientStub {
|
||||
client.fetchRoomEvent = vi.fn(async () => ({}));
|
||||
client.sendTyping = vi.fn(async () => {});
|
||||
client.getRoom = vi.fn(() => ({ hasEncryptionStateEvent: () => false }));
|
||||
client.getRooms = vi.fn(() => []);
|
||||
client.getCrypto = vi.fn(() => undefined);
|
||||
client.decryptEventIfNeeded = vi.fn(async () => {});
|
||||
return client;
|
||||
@@ -141,7 +143,7 @@ let matrixJsClient = createMatrixJsClientStub();
|
||||
let lastCreateClientOpts: Record<string, unknown> | null = null;
|
||||
|
||||
vi.mock("matrix-js-sdk", () => ({
|
||||
ClientEvent: { Event: "event" },
|
||||
ClientEvent: { Event: "event", Room: "Room" },
|
||||
MatrixEventEvent: { Decrypted: "decrypted" },
|
||||
createClient: vi.fn((opts: Record<string, unknown>) => {
|
||||
lastCreateClientOpts = opts;
|
||||
@@ -599,6 +601,46 @@ describe("MatrixClient event bridge", () => {
|
||||
|
||||
expect(invites).toEqual(["!room:example.org"]);
|
||||
});
|
||||
|
||||
it("emits room.invite when SDK emits Room event with invite membership", async () => {
|
||||
const client = new MatrixClient("https://matrix.example.org", "token");
|
||||
const invites: string[] = [];
|
||||
client.on("room.invite", (roomId) => {
|
||||
invites.push(roomId);
|
||||
});
|
||||
|
||||
await client.start();
|
||||
|
||||
matrixJsClient.emit("Room", {
|
||||
roomId: "!invite:example.org",
|
||||
getMyMembership: () => "invite",
|
||||
});
|
||||
|
||||
expect(invites).toEqual(["!invite:example.org"]);
|
||||
});
|
||||
|
||||
it("replays outstanding invite rooms at startup", async () => {
|
||||
matrixJsClient.getRooms = vi.fn(() => [
|
||||
{
|
||||
roomId: "!pending:example.org",
|
||||
getMyMembership: () => "invite",
|
||||
},
|
||||
{
|
||||
roomId: "!joined:example.org",
|
||||
getMyMembership: () => "join",
|
||||
},
|
||||
]);
|
||||
|
||||
const client = new MatrixClient("https://matrix.example.org", "token");
|
||||
const invites: string[] = [];
|
||||
client.on("room.invite", (roomId) => {
|
||||
invites.push(roomId);
|
||||
});
|
||||
|
||||
await client.start();
|
||||
|
||||
expect(invites).toEqual(["!pending:example.org"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient crypto bootstrapping", () => {
|
||||
@@ -642,9 +684,11 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
|
||||
await client.start();
|
||||
|
||||
expect(bootstrapCrossSigning).toHaveBeenCalledWith({
|
||||
setupNewCrossSigning: true,
|
||||
});
|
||||
expect(bootstrapCrossSigning).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
authUploadDeviceSigningKeys: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("provides secret storage callbacks and resolves stored recovery key", async () => {
|
||||
|
||||
@@ -74,6 +74,7 @@ export class MatrixClient {
|
||||
_cryptoStorage?: unknown,
|
||||
opts: {
|
||||
userId?: string;
|
||||
password?: string;
|
||||
deviceId?: string;
|
||||
localTimeoutMs?: number;
|
||||
encryption?: boolean;
|
||||
@@ -123,6 +124,7 @@ export class MatrixClient {
|
||||
});
|
||||
this.cryptoBootstrapper = new MatrixCryptoBootstrapper<MatrixRawEvent>({
|
||||
getUserId: () => this.getUserId(),
|
||||
getPassword: () => opts.password,
|
||||
getDeviceId: () => this.client.getDeviceId(),
|
||||
verificationManager: this.verificationManager,
|
||||
recoveryKeyStore: this.recoveryKeyStore,
|
||||
@@ -175,6 +177,7 @@ export class MatrixClient {
|
||||
initialSyncLimit: this.initialSyncLimit,
|
||||
});
|
||||
this.started = true;
|
||||
this.emitOutstandingInviteEvents();
|
||||
await this.refreshDmCache().catch(noop);
|
||||
}
|
||||
|
||||
@@ -450,6 +453,58 @@ export class MatrixClient {
|
||||
this.decryptBridge.attachEncryptedEvent(event, roomId);
|
||||
}
|
||||
});
|
||||
|
||||
// Some SDK invite transitions are surfaced as room lifecycle events instead of raw timeline events.
|
||||
this.client.on(ClientEvent.Room, (room) => {
|
||||
this.emitMembershipForRoom(room);
|
||||
});
|
||||
}
|
||||
|
||||
private emitMembershipForRoom(room: unknown): void {
|
||||
const roomObj = room as {
|
||||
roomId?: string;
|
||||
getMyMembership?: () => string | null | undefined;
|
||||
selfMembership?: string | null | undefined;
|
||||
};
|
||||
const roomId = roomObj.roomId?.trim();
|
||||
if (!roomId) {
|
||||
return;
|
||||
}
|
||||
const membership = roomObj.getMyMembership?.() ?? roomObj.selfMembership ?? undefined;
|
||||
const selfUserId = this.client.getUserId() ?? this.selfUserId ?? "";
|
||||
if (!selfUserId) {
|
||||
return;
|
||||
}
|
||||
const raw: MatrixRawEvent = {
|
||||
type: "m.room.member",
|
||||
room_id: roomId,
|
||||
sender: selfUserId,
|
||||
state_key: selfUserId,
|
||||
content: { membership },
|
||||
origin_server_ts: Date.now(),
|
||||
unsigned: { age: 0 },
|
||||
};
|
||||
if (membership === "invite") {
|
||||
this.emitter.emit("room.invite", roomId, raw);
|
||||
return;
|
||||
}
|
||||
if (membership === "join") {
|
||||
this.emitter.emit("room.join", roomId, raw);
|
||||
}
|
||||
}
|
||||
|
||||
private emitOutstandingInviteEvents(): void {
|
||||
const listRooms = (this.client as { getRooms?: () => unknown[] }).getRooms;
|
||||
if (typeof listRooms !== "function") {
|
||||
return;
|
||||
}
|
||||
const rooms = listRooms.call(this.client);
|
||||
if (!Array.isArray(rooms)) {
|
||||
return;
|
||||
}
|
||||
for (const room of rooms) {
|
||||
this.emitMembershipForRoom(room);
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshDmCache(): Promise<void> {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MatrixCryptoBootstrapper, type MatrixCryptoBootstrapperDeps } from "./c
|
||||
function createBootstrapperDeps() {
|
||||
return {
|
||||
getUserId: vi.fn(async () => "@bot:example.org"),
|
||||
getPassword: vi.fn(() => "super-secret-password"),
|
||||
getDeviceId: vi.fn(() => "DEVICE123"),
|
||||
verificationManager: {
|
||||
trackVerificationRequest: vi.fn(),
|
||||
@@ -46,15 +47,138 @@ describe("MatrixCryptoBootstrapper", () => {
|
||||
|
||||
await bootstrapper.bootstrap(crypto);
|
||||
|
||||
expect(crypto.bootstrapCrossSigning).toHaveBeenCalledWith({
|
||||
setupNewCrossSigning: true,
|
||||
});
|
||||
expect(crypto.bootstrapCrossSigning).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
authUploadDeviceSigningKeys: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).toHaveBeenCalledWith(
|
||||
crypto,
|
||||
);
|
||||
expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).toHaveBeenCalledTimes(2);
|
||||
expect(deps.decryptBridge.bindCryptoRetrySignals).toHaveBeenCalledWith(crypto);
|
||||
});
|
||||
|
||||
it("forces new cross-signing keys only when readiness check still fails", async () => {
|
||||
const deps = createBootstrapperDeps();
|
||||
const bootstrapCrossSigning = vi.fn(async () => {});
|
||||
const crypto = createCryptoApi({
|
||||
bootstrapCrossSigning,
|
||||
isCrossSigningReady: vi
|
||||
.fn<() => Promise<boolean>>()
|
||||
.mockResolvedValueOnce(false)
|
||||
.mockResolvedValueOnce(true),
|
||||
userHasCrossSigningKeys: vi
|
||||
.fn<() => Promise<boolean>>()
|
||||
.mockResolvedValueOnce(false)
|
||||
.mockResolvedValueOnce(true),
|
||||
getDeviceVerificationStatus: vi.fn(async () => ({
|
||||
isVerified: () => true,
|
||||
})),
|
||||
});
|
||||
const bootstrapper = new MatrixCryptoBootstrapper(
|
||||
deps as unknown as MatrixCryptoBootstrapperDeps<MatrixRawEvent>,
|
||||
);
|
||||
|
||||
await bootstrapper.bootstrap(crypto);
|
||||
|
||||
expect(bootstrapCrossSigning).toHaveBeenCalledTimes(2);
|
||||
expect(bootstrapCrossSigning).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
authUploadDeviceSigningKeys: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
expect(bootstrapCrossSigning).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
setupNewCrossSigning: true,
|
||||
authUploadDeviceSigningKeys: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses password UIA fallback when null and dummy auth fail", async () => {
|
||||
const deps = createBootstrapperDeps();
|
||||
const bootstrapCrossSigning = vi.fn(async () => {});
|
||||
const crypto = createCryptoApi({
|
||||
bootstrapCrossSigning,
|
||||
isCrossSigningReady: vi.fn(async () => true),
|
||||
userHasCrossSigningKeys: vi.fn(async () => true),
|
||||
getDeviceVerificationStatus: vi.fn(async () => ({
|
||||
isVerified: () => true,
|
||||
})),
|
||||
});
|
||||
const bootstrapper = new MatrixCryptoBootstrapper(
|
||||
deps as unknown as MatrixCryptoBootstrapperDeps<MatrixRawEvent>,
|
||||
);
|
||||
|
||||
await bootstrapper.bootstrap(crypto);
|
||||
|
||||
const firstCall = bootstrapCrossSigning.mock.calls[0]?.[0] as {
|
||||
authUploadDeviceSigningKeys?: <T>(
|
||||
makeRequest: (authData: Record<string, unknown> | null) => Promise<T>,
|
||||
) => Promise<T>;
|
||||
};
|
||||
expect(firstCall.authUploadDeviceSigningKeys).toBeTypeOf("function");
|
||||
|
||||
const seenAuthStages: Array<Record<string, unknown> | null> = [];
|
||||
const result = await firstCall.authUploadDeviceSigningKeys?.(async (authData) => {
|
||||
seenAuthStages.push(authData);
|
||||
if (authData === null) {
|
||||
throw new Error("need auth");
|
||||
}
|
||||
if (authData.type === "m.login.dummy") {
|
||||
throw new Error("dummy rejected");
|
||||
}
|
||||
if (authData.type === "m.login.password") {
|
||||
return "ok";
|
||||
}
|
||||
throw new Error("unexpected auth stage");
|
||||
});
|
||||
|
||||
expect(result).toBe("ok");
|
||||
expect(seenAuthStages).toEqual([
|
||||
null,
|
||||
{ type: "m.login.dummy" },
|
||||
{
|
||||
type: "m.login.password",
|
||||
identifier: { type: "m.id.user", user: "@bot:example.org" },
|
||||
password: "super-secret-password",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("resets cross-signing when first bootstrap attempt throws", async () => {
|
||||
const deps = createBootstrapperDeps();
|
||||
const bootstrapCrossSigning = vi
|
||||
.fn<() => Promise<void>>()
|
||||
.mockRejectedValueOnce(new Error("first attempt failed"))
|
||||
.mockResolvedValueOnce(undefined);
|
||||
const crypto = createCryptoApi({
|
||||
bootstrapCrossSigning,
|
||||
isCrossSigningReady: vi.fn(async () => true),
|
||||
userHasCrossSigningKeys: vi.fn(async () => true),
|
||||
getDeviceVerificationStatus: vi.fn(async () => ({
|
||||
isVerified: () => true,
|
||||
})),
|
||||
});
|
||||
const bootstrapper = new MatrixCryptoBootstrapper(
|
||||
deps as unknown as MatrixCryptoBootstrapperDeps<MatrixRawEvent>,
|
||||
);
|
||||
|
||||
await bootstrapper.bootstrap(crypto);
|
||||
|
||||
expect(bootstrapCrossSigning).toHaveBeenCalledTimes(2);
|
||||
expect(bootstrapCrossSigning).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
setupNewCrossSigning: true,
|
||||
authUploadDeviceSigningKeys: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("marks own device verified and cross-signs it when needed", async () => {
|
||||
const deps = createBootstrapperDeps();
|
||||
const setDeviceVerified = vi.fn(async () => {});
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { CryptoEvent } from "matrix-js-sdk/lib/crypto-api/CryptoEvent.js";
|
||||
import type { MatrixDecryptBridge } from "./decrypt-bridge.js";
|
||||
import type { MatrixRecoveryKeyStore } from "./recovery-key-store.js";
|
||||
import type { MatrixCryptoBootstrapApi, MatrixRawEvent } from "./types.js";
|
||||
import type {
|
||||
MatrixAuthDict,
|
||||
MatrixCryptoBootstrapApi,
|
||||
MatrixRawEvent,
|
||||
MatrixUiAuthCallback,
|
||||
} from "./types.js";
|
||||
import type {
|
||||
MatrixVerificationManager,
|
||||
MatrixVerificationRequestLike,
|
||||
@@ -10,6 +15,7 @@ import { LogService } from "./logger.js";
|
||||
|
||||
export type MatrixCryptoBootstrapperDeps<TRawEvent extends MatrixRawEvent> = {
|
||||
getUserId: () => Promise<string>;
|
||||
getPassword?: () => string | undefined;
|
||||
getDeviceId: () => string | null | undefined;
|
||||
verificationManager: MatrixVerificationManager;
|
||||
recoveryKeyStore: MatrixRecoveryKeyStore;
|
||||
@@ -20,19 +26,117 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
constructor(private readonly deps: MatrixCryptoBootstrapperDeps<TRawEvent>) {}
|
||||
|
||||
async bootstrap(crypto: MatrixCryptoBootstrapApi): Promise<void> {
|
||||
await this.bootstrapSecretStorage(crypto);
|
||||
await this.bootstrapCrossSigning(crypto);
|
||||
await this.bootstrapSecretStorage(crypto);
|
||||
await this.ensureOwnDeviceTrust(crypto);
|
||||
this.registerVerificationRequestHandler(crypto);
|
||||
}
|
||||
|
||||
private createSigningKeysUiAuthCallback(params: {
|
||||
userId: string;
|
||||
password?: string;
|
||||
}): MatrixUiAuthCallback {
|
||||
return async <T>(makeRequest: (authData: MatrixAuthDict | null) => Promise<T>): Promise<T> => {
|
||||
try {
|
||||
return await makeRequest(null);
|
||||
} catch {
|
||||
// Some homeservers require an explicit dummy UIA stage even when no user interaction is needed.
|
||||
try {
|
||||
return await makeRequest({ type: "m.login.dummy" });
|
||||
} catch {
|
||||
if (!params.password?.trim()) {
|
||||
throw new Error(
|
||||
"Matrix cross-signing key upload requires UIA; provide matrix.password for m.login.password fallback",
|
||||
);
|
||||
}
|
||||
return await makeRequest({
|
||||
type: "m.login.password",
|
||||
identifier: { type: "m.id.user", user: params.userId },
|
||||
password: params.password,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async bootstrapCrossSigning(crypto: MatrixCryptoBootstrapApi): Promise<void> {
|
||||
const userId = await this.deps.getUserId();
|
||||
const authUploadDeviceSigningKeys = this.createSigningKeysUiAuthCallback({
|
||||
userId,
|
||||
password: this.deps.getPassword?.(),
|
||||
});
|
||||
const hasPublishedCrossSigningKeys = async (): Promise<boolean> => {
|
||||
if (typeof crypto.userHasCrossSigningKeys !== "function") {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return await crypto.userHasCrossSigningKeys(userId, true);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const isCrossSigningReady = async (): Promise<boolean> => {
|
||||
if (typeof crypto.isCrossSigningReady !== "function") {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return await crypto.isCrossSigningReady();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// First pass: preserve existing cross-signing identity and ensure public keys are uploaded.
|
||||
try {
|
||||
await crypto.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||
LogService.info("MatrixClientLite", "Cross-signing bootstrap complete");
|
||||
await crypto.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
} catch (err) {
|
||||
LogService.warn("MatrixClientLite", "Failed to bootstrap cross-signing:", err);
|
||||
LogService.warn(
|
||||
"MatrixClientLite",
|
||||
"Initial cross-signing bootstrap failed, trying reset:",
|
||||
err,
|
||||
);
|
||||
try {
|
||||
await crypto.bootstrapCrossSigning({
|
||||
setupNewCrossSigning: true,
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
} catch (resetErr) {
|
||||
LogService.warn("MatrixClientLite", "Failed to bootstrap cross-signing:", resetErr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const firstPassReady = await isCrossSigningReady();
|
||||
const firstPassPublished = await hasPublishedCrossSigningKeys();
|
||||
if (firstPassReady && firstPassPublished) {
|
||||
LogService.info("MatrixClientLite", "Cross-signing bootstrap complete");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: recover from broken local/server state by creating a fresh identity.
|
||||
try {
|
||||
await crypto.bootstrapCrossSigning({
|
||||
setupNewCrossSigning: true,
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
} catch (err) {
|
||||
LogService.warn("MatrixClientLite", "Fallback cross-signing bootstrap failed:", err);
|
||||
return;
|
||||
}
|
||||
|
||||
const finalReady = await isCrossSigningReady();
|
||||
const finalPublished = await hasPublishedCrossSigningKeys();
|
||||
if (finalReady && finalPublished) {
|
||||
LogService.info("MatrixClientLite", "Cross-signing bootstrap complete");
|
||||
return;
|
||||
}
|
||||
LogService.warn(
|
||||
"MatrixClientLite",
|
||||
"Cross-signing bootstrap finished but server keys are still not published",
|
||||
);
|
||||
}
|
||||
|
||||
private async bootstrapSecretStorage(crypto: MatrixCryptoBootstrapApi): Promise<void> {
|
||||
|
||||
@@ -135,4 +135,42 @@ describe("MatrixRecoveryKeyStore", () => {
|
||||
keyId: "NEW",
|
||||
});
|
||||
});
|
||||
|
||||
it("recreates secret storage when default key exists but is not usable locally", async () => {
|
||||
const recoveryKeyPath = createTempRecoveryKeyPath();
|
||||
const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
|
||||
const generated = {
|
||||
keyId: "RECOVERED",
|
||||
keyInfo: { name: "recovered" },
|
||||
privateKey: new Uint8Array([1, 1, 2, 3]),
|
||||
encodedPrivateKey: "encoded-recovered-key",
|
||||
};
|
||||
const createRecoveryKeyFromPassphrase = vi.fn(async () => generated);
|
||||
const bootstrapSecretStorage = vi.fn(
|
||||
async (opts?: { createSecretStorageKey?: () => Promise<unknown> }) => {
|
||||
await opts?.createSecretStorageKey?.();
|
||||
},
|
||||
);
|
||||
const crypto = {
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
bootstrapSecretStorage,
|
||||
createRecoveryKeyFromPassphrase,
|
||||
getSecretStorageStatus: vi.fn(async () => ({ ready: false, defaultKeyId: "LEGACY" })),
|
||||
requestOwnUserVerification: vi.fn(async () => null),
|
||||
} as unknown as MatrixCryptoBootstrapApi;
|
||||
|
||||
await store.bootstrapSecretStorageWithRecoveryKey(crypto);
|
||||
|
||||
expect(createRecoveryKeyFromPassphrase).toHaveBeenCalledTimes(1);
|
||||
expect(bootstrapSecretStorage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
setupNewSecretStorage: true,
|
||||
}),
|
||||
);
|
||||
expect(store.getRecoveryKeySummary()).toMatchObject({
|
||||
keyId: "RECOVERED",
|
||||
encodedPrivateKey: "encoded-recovered-key",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,6 +99,9 @@ export class MatrixRecoveryKeyStore {
|
||||
}
|
||||
|
||||
const hasDefaultSecretStorageKey = Boolean(status?.defaultKeyId);
|
||||
const hasKnownInvalidSecrets = Object.values(status?.secretStorageKeyValidityMap ?? {}).some(
|
||||
(valid) => valid === false,
|
||||
);
|
||||
let generatedRecoveryKey = false;
|
||||
const storedRecovery = this.loadStoredRecoveryKey();
|
||||
let recoveryKey = storedRecovery
|
||||
@@ -137,6 +140,16 @@ export class MatrixRecoveryKeyStore {
|
||||
return recoveryKey;
|
||||
};
|
||||
|
||||
const shouldRecreateSecretStorage =
|
||||
!hasDefaultSecretStorageKey ||
|
||||
(!recoveryKey && status?.ready === false) ||
|
||||
hasKnownInvalidSecrets;
|
||||
|
||||
if (hasKnownInvalidSecrets) {
|
||||
// Existing secret storage keys can't decrypt required secrets. Generate a fresh recovery key.
|
||||
recoveryKey = null;
|
||||
}
|
||||
|
||||
const secretStorageOptions: {
|
||||
createSecretStorageKey?: () => Promise<MatrixGeneratedSecretStorageKey>;
|
||||
setupNewSecretStorage?: boolean;
|
||||
@@ -145,7 +158,7 @@ export class MatrixRecoveryKeyStore {
|
||||
setupNewKeyBackup: false,
|
||||
};
|
||||
|
||||
if (!hasDefaultSecretStorageKey) {
|
||||
if (shouldRecreateSecretStorage) {
|
||||
secretStorageOptions.setupNewSecretStorage = true;
|
||||
secretStorageOptions.createSecretStorageKey = ensureRecoveryKey;
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ export type LocationMessageEventContent = MessageEventContent & {
|
||||
export type MatrixSecretStorageStatus = {
|
||||
ready: boolean;
|
||||
defaultKeyId: string | null;
|
||||
secretStorageKeyValidityMap?: Record<string, boolean>;
|
||||
};
|
||||
|
||||
export type MatrixGeneratedSecretStorageKey = {
|
||||
@@ -143,9 +144,18 @@ export type MatrixStoredRecoveryKey = {
|
||||
};
|
||||
};
|
||||
|
||||
export type MatrixAuthDict = Record<string, unknown>;
|
||||
|
||||
export type MatrixUiAuthCallback = <T>(
|
||||
makeRequest: (authData: MatrixAuthDict | null) => Promise<T>,
|
||||
) => Promise<T>;
|
||||
|
||||
export type MatrixCryptoBootstrapApi = {
|
||||
on: (eventName: string, listener: (...args: unknown[]) => void) => void;
|
||||
bootstrapCrossSigning: (opts: { setupNewCrossSigning?: boolean }) => Promise<void>;
|
||||
bootstrapCrossSigning: (opts: {
|
||||
setupNewCrossSigning?: boolean;
|
||||
authUploadDeviceSigningKeys?: MatrixUiAuthCallback;
|
||||
}) => Promise<void>;
|
||||
bootstrapSecretStorage: (opts?: {
|
||||
createSecretStorageKey?: () => Promise<MatrixGeneratedSecretStorageKey>;
|
||||
setupNewSecretStorage?: boolean;
|
||||
@@ -169,4 +179,5 @@ export type MatrixCryptoBootstrapApi = {
|
||||
setDeviceVerified?: (userId: string, deviceId: string, verified?: boolean) => Promise<void>;
|
||||
crossSignDevice?: (deviceId: string) => Promise<void>;
|
||||
isCrossSigningReady?: () => Promise<boolean>;
|
||||
userHasCrossSigningKeys?: (userId?: string, downloadUncached?: boolean) => Promise<boolean>;
|
||||
};
|
||||
|
||||
@@ -49,6 +49,7 @@ export async function resolveMatrixClient(opts: {
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: auth.encryption,
|
||||
localTimeoutMs: opts.timeoutMs,
|
||||
|
||||
Reference in New Issue
Block a user