mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
Matrix: honor env-backed legacy migration config
This commit is contained in:
@@ -195,4 +195,67 @@ describe("matrix legacy encrypted-state migration", () => {
|
||||
expect(state.accountId).toBe("ops");
|
||||
});
|
||||
});
|
||||
|
||||
it("uses scoped Matrix env vars when resolving flat legacy crypto migration", async () => {
|
||||
await withTempHome(
|
||||
async (home) => {
|
||||
const stateDir = path.join(home, ".openclaw");
|
||||
writeFile(
|
||||
path.join(stateDir, "matrix", "crypto", "bot-sdk.json"),
|
||||
JSON.stringify({ deviceId: "DEVICEOPS" }),
|
||||
);
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
matrix: {
|
||||
accounts: {
|
||||
ops: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const { rootDir } = resolveMatrixAccountStorageRoot({
|
||||
stateDir,
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@ops-bot:example.org",
|
||||
accessToken: "tok-ops-env",
|
||||
accountId: "ops",
|
||||
});
|
||||
|
||||
const detection = detectLegacyMatrixCrypto({ cfg, env: process.env });
|
||||
expect(detection.warnings).toEqual([]);
|
||||
expect(detection.plans).toHaveLength(1);
|
||||
expect(detection.plans[0]?.accountId).toBe("ops");
|
||||
|
||||
const result = await autoPrepareLegacyMatrixCrypto({
|
||||
cfg,
|
||||
env: process.env,
|
||||
deps: {
|
||||
inspectLegacyStore: async () => ({
|
||||
deviceId: "DEVICEOPS",
|
||||
roomKeyCounts: { total: 4, backedUp: 4 },
|
||||
backupVersion: "9001",
|
||||
decryptionKeyBase64: "YWJjZA==",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(true);
|
||||
expect(result.warnings).toEqual([]);
|
||||
const recovery = JSON.parse(
|
||||
fs.readFileSync(path.join(rootDir, "recovery-key.json"), "utf8"),
|
||||
) as {
|
||||
privateKeyBase64: string;
|
||||
};
|
||||
expect(recovery.privateKeyBase64).toBe("YWJjZA==");
|
||||
},
|
||||
{
|
||||
env: {
|
||||
MATRIX_OPS_HOMESERVER: "https://matrix.example.org",
|
||||
MATRIX_OPS_USER_ID: "@ops-bot:example.org",
|
||||
MATRIX_OPS_ACCESS_TOKEN: "tok-ops-env",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,25 +5,22 @@ import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { writeJsonFileAtomically } from "../plugin-sdk/json-store.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||
import {
|
||||
resolveConfiguredMatrixAccountIds,
|
||||
resolveMatrixChannelConfig,
|
||||
resolveMatrixDefaultOrOnlyAccountId,
|
||||
} from "./matrix-account-selection.js";
|
||||
import {
|
||||
credentialsMatchResolvedIdentity,
|
||||
loadStoredMatrixCredentials,
|
||||
resolveMatrixMigrationConfigFields,
|
||||
} from "./matrix-migration-config.js";
|
||||
import {
|
||||
resolveMatrixAccountStorageRoot,
|
||||
resolveMatrixCredentialsPath,
|
||||
resolveMatrixLegacyFlatStoragePaths,
|
||||
} from "./matrix-storage-paths.js";
|
||||
|
||||
type MatrixStoredCredentials = {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
deviceId?: string;
|
||||
};
|
||||
|
||||
type MatrixLegacyCryptoCounts = {
|
||||
total: number;
|
||||
backedUp: number;
|
||||
@@ -99,10 +96,6 @@ type MatrixStoredRecoveryKey = {
|
||||
};
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isLegacyBotSdkCryptoStore(cryptoRootDir: string): boolean {
|
||||
return (
|
||||
fs.existsSync(path.join(cryptoRootDir, "bot-sdk.json")) ||
|
||||
@@ -117,40 +110,6 @@ function isLegacyBotSdkCryptoStore(cryptoRootDir: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function loadStoredMatrixCredentials(
|
||||
env: NodeJS.ProcessEnv,
|
||||
accountId: string,
|
||||
): MatrixStoredCredentials | null {
|
||||
const stateDir = resolveStateDir(env, os.homedir);
|
||||
const credentialsPath = resolveMatrixCredentialsPath({
|
||||
stateDir,
|
||||
accountId: normalizeAccountId(accountId),
|
||||
});
|
||||
try {
|
||||
if (!fs.existsSync(credentialsPath)) {
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(
|
||||
fs.readFileSync(credentialsPath, "utf8"),
|
||||
) as Partial<MatrixStoredCredentials>;
|
||||
if (
|
||||
typeof parsed.homeserver !== "string" ||
|
||||
typeof parsed.userId !== "string" ||
|
||||
typeof parsed.accessToken !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
homeserver: parsed.homeserver,
|
||||
userId: parsed.userId,
|
||||
accessToken: parsed.accessToken,
|
||||
deviceId: typeof parsed.deviceId === "string" ? parsed.deviceId : undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveMatrixAccountIds(cfg: OpenClawConfig): string[] {
|
||||
return resolveConfiguredMatrixAccountIds(cfg);
|
||||
}
|
||||
@@ -159,24 +118,6 @@ function resolveMatrixFlatStoreTargetAccountId(cfg: OpenClawConfig): string {
|
||||
return resolveMatrixDefaultOrOnlyAccountId(cfg);
|
||||
}
|
||||
|
||||
function resolveMatrixAccountConfig(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
): Record<string, unknown> {
|
||||
const channel = resolveMatrixChannelConfig(cfg);
|
||||
if (!channel) {
|
||||
return {};
|
||||
}
|
||||
const accounts = isRecord(channel.accounts) ? channel.accounts : null;
|
||||
const accountEntry = accounts && isRecord(accounts[accountId]) ? accounts[accountId] : null;
|
||||
const merged = {
|
||||
...channel,
|
||||
...accountEntry,
|
||||
};
|
||||
delete merged.accounts;
|
||||
return merged;
|
||||
}
|
||||
|
||||
function resolveLegacyMatrixFlatStorePlan(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
@@ -197,14 +138,20 @@ function resolveLegacyMatrixFlatStorePlan(params: {
|
||||
|
||||
const accountId = resolveMatrixFlatStoreTargetAccountId(params.cfg);
|
||||
const stored = loadStoredMatrixCredentials(params.env, accountId);
|
||||
const account = resolveMatrixAccountConfig(params.cfg, accountId);
|
||||
const homeserver = typeof account.homeserver === "string" ? account.homeserver.trim() : "";
|
||||
const userId =
|
||||
(typeof account.userId === "string" ? account.userId.trim() : "") || stored?.userId || "";
|
||||
const accessToken =
|
||||
(typeof account.accessToken === "string" ? account.accessToken.trim() : "") ||
|
||||
stored?.accessToken ||
|
||||
"";
|
||||
const resolved = resolveMatrixMigrationConfigFields({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
accountId,
|
||||
});
|
||||
const matchingStored = credentialsMatchResolvedIdentity(stored, {
|
||||
homeserver: resolved.homeserver,
|
||||
userId: resolved.userId,
|
||||
})
|
||||
? stored
|
||||
: null;
|
||||
const homeserver = resolved.homeserver;
|
||||
const userId = resolved.userId || matchingStored?.userId || "";
|
||||
const accessToken = resolved.accessToken || matchingStored?.accessToken || "";
|
||||
|
||||
if (!homeserver || !userId || !accessToken) {
|
||||
return {
|
||||
@@ -274,18 +221,21 @@ function resolveMatrixLegacyCryptoPlans(params: {
|
||||
|
||||
const stateDir = resolveStateDir(params.env, os.homedir);
|
||||
for (const accountId of resolveMatrixAccountIds(params.cfg)) {
|
||||
const account = resolveMatrixAccountConfig(params.cfg, accountId);
|
||||
const stored = loadStoredMatrixCredentials(params.env, accountId);
|
||||
const homeserver =
|
||||
(typeof account.homeserver === "string" ? account.homeserver.trim() : "") ||
|
||||
stored?.homeserver ||
|
||||
"";
|
||||
const userId =
|
||||
(typeof account.userId === "string" ? account.userId.trim() : "") || stored?.userId || "";
|
||||
const accessToken =
|
||||
(typeof account.accessToken === "string" ? account.accessToken.trim() : "") ||
|
||||
stored?.accessToken ||
|
||||
"";
|
||||
const resolved = resolveMatrixMigrationConfigFields({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
accountId,
|
||||
});
|
||||
const matchingStored = credentialsMatchResolvedIdentity(stored, {
|
||||
homeserver: resolved.homeserver,
|
||||
userId: resolved.userId,
|
||||
})
|
||||
? stored
|
||||
: null;
|
||||
const homeserver = resolved.homeserver;
|
||||
const userId = resolved.userId || matchingStored?.userId || "";
|
||||
const accessToken = resolved.accessToken || matchingStored?.accessToken || "";
|
||||
if (!homeserver || !userId || !accessToken) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -120,6 +120,48 @@ describe("matrix legacy state migration", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses scoped Matrix env vars when resolving a flat-store migration target", async () => {
|
||||
await withTempHome(
|
||||
async (home) => {
|
||||
const stateDir = path.join(home, ".openclaw");
|
||||
writeFile(path.join(stateDir, "matrix", "bot-storage.json"), '{"next_batch":"s1"}');
|
||||
writeFile(path.join(stateDir, "matrix", "crypto", "store.db"), "crypto");
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
matrix: {
|
||||
accounts: {
|
||||
ops: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const detection = detectLegacyMatrixState({ cfg, env: process.env });
|
||||
expect(detection && "warning" in detection).toBe(false);
|
||||
if (!detection || "warning" in detection) {
|
||||
throw new Error("expected scoped Matrix env vars to resolve a legacy state plan");
|
||||
}
|
||||
|
||||
expect(detection.accountId).toBe("ops");
|
||||
expect(detection.targetRootDir).toContain("matrix.example.org__ops-bot_example.org");
|
||||
|
||||
const result = await autoMigrateLegacyMatrixState({ cfg, env: process.env });
|
||||
expect(result.migrated).toBe(true);
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(fs.existsSync(detection.targetStoragePath)).toBe(true);
|
||||
expect(fs.existsSync(path.join(detection.targetCryptoPath, "store.db"))).toBe(true);
|
||||
},
|
||||
{
|
||||
env: {
|
||||
MATRIX_OPS_HOMESERVER: "https://matrix.example.org",
|
||||
MATRIX_OPS_USER_ID: "@ops-bot:example.org",
|
||||
MATRIX_OPS_ACCESS_TOKEN: "tok-ops-env",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("migrates flat legacy Matrix state into the only configured non-default account", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const stateDir = path.join(home, ".openclaw");
|
||||
|
||||
@@ -8,18 +8,16 @@ import {
|
||||
resolveMatrixChannelConfig,
|
||||
resolveMatrixDefaultOrOnlyAccountId,
|
||||
} from "./matrix-account-selection.js";
|
||||
import {
|
||||
credentialsMatchResolvedIdentity,
|
||||
loadStoredMatrixCredentials,
|
||||
resolveMatrixMigrationConfigFields,
|
||||
} from "./matrix-migration-config.js";
|
||||
import {
|
||||
resolveMatrixAccountStorageRoot,
|
||||
resolveMatrixCredentialsPath as resolveSharedMatrixCredentialsPath,
|
||||
resolveMatrixLegacyFlatStoragePaths,
|
||||
} from "./matrix-storage-paths.js";
|
||||
|
||||
type MatrixStoredCredentials = {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
};
|
||||
|
||||
export type MatrixLegacyStateMigrationResult = {
|
||||
migrated: boolean;
|
||||
changes: string[];
|
||||
@@ -49,63 +47,6 @@ function resolveLegacyMatrixPaths(env: NodeJS.ProcessEnv): {
|
||||
return resolveMatrixLegacyFlatStoragePaths(stateDir);
|
||||
}
|
||||
|
||||
function resolveMatrixCredentialsPath(env: NodeJS.ProcessEnv, accountId: string): string {
|
||||
const stateDir = resolveStateDir(env, os.homedir);
|
||||
return resolveSharedMatrixCredentialsPath({
|
||||
stateDir,
|
||||
accountId: normalizeAccountId(accountId),
|
||||
});
|
||||
}
|
||||
|
||||
function loadStoredMatrixCredentials(
|
||||
env: NodeJS.ProcessEnv,
|
||||
accountId: string,
|
||||
): MatrixStoredCredentials | null {
|
||||
const credentialsPath = resolveMatrixCredentialsPath(env, accountId);
|
||||
try {
|
||||
if (!fs.existsSync(credentialsPath)) {
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(
|
||||
fs.readFileSync(credentialsPath, "utf-8"),
|
||||
) as Partial<MatrixStoredCredentials>;
|
||||
if (
|
||||
typeof parsed.homeserver !== "string" ||
|
||||
typeof parsed.userId !== "string" ||
|
||||
typeof parsed.accessToken !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
homeserver: parsed.homeserver,
|
||||
userId: parsed.userId,
|
||||
accessToken: parsed.accessToken,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveMatrixAccountConfig(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
): Record<string, unknown> {
|
||||
const channel = resolveMatrixChannelConfig(cfg);
|
||||
if (!channel) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const accounts = isRecord(channel.accounts) ? channel.accounts : null;
|
||||
const accountEntry = accounts && isRecord(accounts[accountId]) ? accounts[accountId] : null;
|
||||
|
||||
const merged = {
|
||||
...channel,
|
||||
...accountEntry,
|
||||
};
|
||||
delete merged.accounts;
|
||||
return merged;
|
||||
}
|
||||
|
||||
function resolveMatrixTargetAccountId(cfg: OpenClawConfig): string {
|
||||
return resolveMatrixDefaultOrOnlyAccountId(cfg);
|
||||
}
|
||||
@@ -155,25 +96,22 @@ function resolveMatrixMigrationPlan(params: {
|
||||
}
|
||||
|
||||
const accountId = resolveMatrixTargetAccountId(params.cfg);
|
||||
const account = resolveMatrixAccountConfig(params.cfg, accountId);
|
||||
const stored = loadStoredMatrixCredentials(params.env, accountId);
|
||||
const selectionNote = resolveMatrixFlatStoreSelectionNote({ channel, accountId });
|
||||
|
||||
const homeserver = typeof account.homeserver === "string" ? account.homeserver.trim() : "";
|
||||
const configUserId = typeof account.userId === "string" ? account.userId.trim() : "";
|
||||
const configAccessToken =
|
||||
typeof account.accessToken === "string" ? account.accessToken.trim() : "";
|
||||
|
||||
const storedMatchesHomeserver =
|
||||
stored && homeserver ? stored.homeserver === homeserver : Boolean(stored);
|
||||
const storedMatchesUser =
|
||||
stored && configUserId ? stored.userId === configUserId : Boolean(stored);
|
||||
|
||||
const userId =
|
||||
configUserId || (storedMatchesHomeserver && storedMatchesUser ? (stored?.userId ?? "") : "");
|
||||
const accessToken =
|
||||
configAccessToken ||
|
||||
(storedMatchesHomeserver && storedMatchesUser ? (stored?.accessToken ?? "") : "");
|
||||
const resolved = resolveMatrixMigrationConfigFields({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
accountId,
|
||||
});
|
||||
const matchingStored = credentialsMatchResolvedIdentity(stored, {
|
||||
homeserver: resolved.homeserver,
|
||||
userId: resolved.userId,
|
||||
})
|
||||
? stored
|
||||
: null;
|
||||
const homeserver = resolved.homeserver;
|
||||
const userId = resolved.userId || matchingStored?.userId || "";
|
||||
const accessToken = resolved.accessToken || matchingStored?.accessToken || "";
|
||||
|
||||
if (!homeserver || !userId || !accessToken) {
|
||||
return {
|
||||
|
||||
149
src/infra/matrix-migration-config.ts
Normal file
149
src/infra/matrix-migration-config.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
import { resolveMatrixChannelConfig } from "./matrix-account-selection.js";
|
||||
import { resolveMatrixCredentialsPath } from "./matrix-storage-paths.js";
|
||||
|
||||
export type MatrixStoredCredentials = {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
deviceId?: string;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function clean(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function resolveMatrixEnvAccountToken(accountId: string): string {
|
||||
return normalizeAccountId(accountId)
|
||||
.replace(/[^a-z0-9]+/gi, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
function resolveScopedMatrixEnvConfig(
|
||||
accountId: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
} {
|
||||
const token = resolveMatrixEnvAccountToken(accountId);
|
||||
return {
|
||||
homeserver: clean(env[`MATRIX_${token}_HOMESERVER`]),
|
||||
userId: clean(env[`MATRIX_${token}_USER_ID`]),
|
||||
accessToken: clean(env[`MATRIX_${token}_ACCESS_TOKEN`]),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveGlobalMatrixEnvConfig(env: NodeJS.ProcessEnv): {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
} {
|
||||
return {
|
||||
homeserver: clean(env.MATRIX_HOMESERVER),
|
||||
userId: clean(env.MATRIX_USER_ID),
|
||||
accessToken: clean(env.MATRIX_ACCESS_TOKEN),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMatrixAccountConfigEntry(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
): Record<string, unknown> | null {
|
||||
const channel = resolveMatrixChannelConfig(cfg);
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
const accounts = isRecord(channel.accounts) ? channel.accounts : null;
|
||||
return accounts && isRecord(accounts[accountId]) ? accounts[accountId] : null;
|
||||
}
|
||||
|
||||
export function resolveMatrixMigrationConfigFields(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
accountId: string;
|
||||
}): {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
} {
|
||||
const channel = resolveMatrixChannelConfig(params.cfg);
|
||||
const account = resolveMatrixAccountConfigEntry(params.cfg, params.accountId);
|
||||
const scopedEnv = resolveScopedMatrixEnvConfig(params.accountId, params.env);
|
||||
const globalEnv = resolveGlobalMatrixEnvConfig(params.env);
|
||||
|
||||
return {
|
||||
homeserver:
|
||||
clean(account?.homeserver) ||
|
||||
scopedEnv.homeserver ||
|
||||
clean(channel?.homeserver) ||
|
||||
globalEnv.homeserver,
|
||||
userId:
|
||||
clean(account?.userId) || scopedEnv.userId || clean(channel?.userId) || globalEnv.userId,
|
||||
accessToken:
|
||||
clean(account?.accessToken) ||
|
||||
scopedEnv.accessToken ||
|
||||
clean(channel?.accessToken) ||
|
||||
globalEnv.accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadStoredMatrixCredentials(
|
||||
env: NodeJS.ProcessEnv,
|
||||
accountId: string,
|
||||
): MatrixStoredCredentials | null {
|
||||
const stateDir = resolveStateDir(env, os.homedir);
|
||||
const credentialsPath = resolveMatrixCredentialsPath({
|
||||
stateDir,
|
||||
accountId: normalizeAccountId(accountId),
|
||||
});
|
||||
try {
|
||||
if (!fs.existsSync(credentialsPath)) {
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(
|
||||
fs.readFileSync(credentialsPath, "utf8"),
|
||||
) as Partial<MatrixStoredCredentials>;
|
||||
if (
|
||||
typeof parsed.homeserver !== "string" ||
|
||||
typeof parsed.userId !== "string" ||
|
||||
typeof parsed.accessToken !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
homeserver: parsed.homeserver,
|
||||
userId: parsed.userId,
|
||||
accessToken: parsed.accessToken,
|
||||
deviceId: typeof parsed.deviceId === "string" ? parsed.deviceId : undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function credentialsMatchResolvedIdentity(
|
||||
stored: MatrixStoredCredentials | null,
|
||||
identity: {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
},
|
||||
): stored is MatrixStoredCredentials {
|
||||
if (!stored || !identity.homeserver) {
|
||||
return false;
|
||||
}
|
||||
if (!identity.userId) {
|
||||
return stored.homeserver === identity.homeserver;
|
||||
}
|
||||
return stored.homeserver === identity.homeserver && stored.userId === identity.userId;
|
||||
}
|
||||
Reference in New Issue
Block a user