mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:31:06 +00:00
Matrix: restore startup and doctor migration
This commit is contained in:
4
extensions/matrix/runtime-api.ts
Normal file
4
extensions/matrix/runtime-api.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./src/account-selection.js";
|
||||
export * from "./src/auth-precedence.js";
|
||||
export * from "./src/env-vars.js";
|
||||
export * from "./src/storage-paths.js";
|
||||
106
extensions/matrix/src/account-selection.ts
Normal file
106
extensions/matrix/src/account-selection.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { listMatrixEnvAccountIds } from "./env-vars.js";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function resolveMatrixChannelConfig(cfg: OpenClawConfig): Record<string, unknown> | null {
|
||||
return isRecord(cfg.channels?.matrix) ? cfg.channels.matrix : null;
|
||||
}
|
||||
|
||||
export function findMatrixAccountEntry(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
): Record<string, unknown> | null {
|
||||
const channel = resolveMatrixChannelConfig(cfg);
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accounts = isRecord(channel.accounts) ? channel.accounts : null;
|
||||
if (!accounts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedAccountId = normalizeAccountId(accountId);
|
||||
for (const [rawAccountId, value] of Object.entries(accounts)) {
|
||||
if (normalizeAccountId(rawAccountId) === normalizedAccountId && isRecord(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveConfiguredMatrixAccountIds(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
const channel = resolveMatrixChannelConfig(cfg);
|
||||
const ids = new Set<string>(listMatrixEnvAccountIds(env));
|
||||
|
||||
const accounts = channel && isRecord(channel.accounts) ? channel.accounts : null;
|
||||
if (accounts) {
|
||||
for (const [accountId, value] of Object.entries(accounts)) {
|
||||
if (isRecord(value)) {
|
||||
ids.add(normalizeAccountId(accountId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.size === 0 && channel) {
|
||||
ids.add(DEFAULT_ACCOUNT_ID);
|
||||
}
|
||||
|
||||
return Array.from(ids).toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export function resolveMatrixDefaultOrOnlyAccountId(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
const channel = resolveMatrixChannelConfig(cfg);
|
||||
if (!channel) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
const configuredDefault = normalizeOptionalAccountId(
|
||||
typeof channel.defaultAccount === "string" ? channel.defaultAccount : undefined,
|
||||
);
|
||||
const configuredAccountIds = resolveConfiguredMatrixAccountIds(cfg, env);
|
||||
if (configuredDefault && configuredAccountIds.includes(configuredDefault)) {
|
||||
return configuredDefault;
|
||||
}
|
||||
if (configuredAccountIds.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
if (configuredAccountIds.length === 1) {
|
||||
return configuredAccountIds[0] ?? DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
export function requiresExplicitMatrixDefaultAccount(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
const channel = resolveMatrixChannelConfig(cfg);
|
||||
if (!channel) {
|
||||
return false;
|
||||
}
|
||||
const configuredAccountIds = resolveConfiguredMatrixAccountIds(cfg, env);
|
||||
if (configuredAccountIds.length <= 1) {
|
||||
return false;
|
||||
}
|
||||
const configuredDefault = normalizeOptionalAccountId(
|
||||
typeof channel.defaultAccount === "string" ? channel.defaultAccount : undefined,
|
||||
);
|
||||
return !(configuredDefault && configuredAccountIds.includes(configuredDefault));
|
||||
}
|
||||
61
extensions/matrix/src/auth-precedence.ts
Normal file
61
extensions/matrix/src/auth-precedence.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
|
||||
export type MatrixResolvedStringField =
|
||||
| "homeserver"
|
||||
| "userId"
|
||||
| "accessToken"
|
||||
| "password"
|
||||
| "deviceId"
|
||||
| "deviceName";
|
||||
|
||||
export type MatrixResolvedStringValues = Record<MatrixResolvedStringField, string>;
|
||||
|
||||
type MatrixStringSourceMap = Partial<Record<MatrixResolvedStringField, string>>;
|
||||
|
||||
const MATRIX_DEFAULT_ACCOUNT_AUTH_ONLY_FIELDS = new Set<MatrixResolvedStringField>([
|
||||
"userId",
|
||||
"accessToken",
|
||||
"password",
|
||||
"deviceId",
|
||||
]);
|
||||
|
||||
function resolveMatrixStringSourceValue(value: string | undefined): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function shouldAllowBaseAuthFallback(accountId: string, field: MatrixResolvedStringField): boolean {
|
||||
return (
|
||||
normalizeAccountId(accountId) === DEFAULT_ACCOUNT_ID ||
|
||||
!MATRIX_DEFAULT_ACCOUNT_AUTH_ONLY_FIELDS.has(field)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveMatrixAccountStringValues(params: {
|
||||
accountId: string;
|
||||
account?: MatrixStringSourceMap;
|
||||
scopedEnv?: MatrixStringSourceMap;
|
||||
channel?: MatrixStringSourceMap;
|
||||
globalEnv?: MatrixStringSourceMap;
|
||||
}): MatrixResolvedStringValues {
|
||||
const fields: MatrixResolvedStringField[] = [
|
||||
"homeserver",
|
||||
"userId",
|
||||
"accessToken",
|
||||
"password",
|
||||
"deviceId",
|
||||
"deviceName",
|
||||
];
|
||||
const resolved = {} as MatrixResolvedStringValues;
|
||||
|
||||
for (const field of fields) {
|
||||
resolved[field] =
|
||||
resolveMatrixStringSourceValue(params.account?.[field]) ||
|
||||
resolveMatrixStringSourceValue(params.scopedEnv?.[field]) ||
|
||||
(shouldAllowBaseAuthFallback(params.accountId, field)
|
||||
? resolveMatrixStringSourceValue(params.channel?.[field]) ||
|
||||
resolveMatrixStringSourceValue(params.globalEnv?.[field])
|
||||
: "");
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
92
extensions/matrix/src/env-vars.ts
Normal file
92
extensions/matrix/src/env-vars.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { normalizeAccountId, normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
|
||||
const MATRIX_SCOPED_ENV_SUFFIXES = [
|
||||
"HOMESERVER",
|
||||
"USER_ID",
|
||||
"ACCESS_TOKEN",
|
||||
"PASSWORD",
|
||||
"DEVICE_ID",
|
||||
"DEVICE_NAME",
|
||||
] as const;
|
||||
const MATRIX_GLOBAL_ENV_KEYS = MATRIX_SCOPED_ENV_SUFFIXES.map((suffix) => `MATRIX_${suffix}`);
|
||||
|
||||
const MATRIX_SCOPED_ENV_RE = new RegExp(`^MATRIX_(.+)_(${MATRIX_SCOPED_ENV_SUFFIXES.join("|")})$`);
|
||||
|
||||
export function resolveMatrixEnvAccountToken(accountId: string): string {
|
||||
return Array.from(normalizeAccountId(accountId))
|
||||
.map((char) =>
|
||||
/[a-z0-9]/.test(char)
|
||||
? char.toUpperCase()
|
||||
: `_X${char.codePointAt(0)?.toString(16).toUpperCase() ?? "00"}_`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function getMatrixScopedEnvVarNames(accountId: string): {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
password: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
} {
|
||||
const token = resolveMatrixEnvAccountToken(accountId);
|
||||
return {
|
||||
homeserver: `MATRIX_${token}_HOMESERVER`,
|
||||
userId: `MATRIX_${token}_USER_ID`,
|
||||
accessToken: `MATRIX_${token}_ACCESS_TOKEN`,
|
||||
password: `MATRIX_${token}_PASSWORD`,
|
||||
deviceId: `MATRIX_${token}_DEVICE_ID`,
|
||||
deviceName: `MATRIX_${token}_DEVICE_NAME`,
|
||||
};
|
||||
}
|
||||
|
||||
function decodeMatrixEnvAccountToken(token: string): string | undefined {
|
||||
let decoded = "";
|
||||
for (let index = 0; index < token.length; ) {
|
||||
const hexEscape = /^_X([0-9A-F]+)_/.exec(token.slice(index));
|
||||
if (hexEscape) {
|
||||
const hex = hexEscape[1];
|
||||
const codePoint = hex ? Number.parseInt(hex, 16) : Number.NaN;
|
||||
if (!Number.isFinite(codePoint)) {
|
||||
return undefined;
|
||||
}
|
||||
const char = String.fromCodePoint(codePoint);
|
||||
decoded += char;
|
||||
index += hexEscape[0].length;
|
||||
continue;
|
||||
}
|
||||
const char = token[index];
|
||||
if (!char || !/[A-Z0-9]/.test(char)) {
|
||||
return undefined;
|
||||
}
|
||||
decoded += char.toLowerCase();
|
||||
index += 1;
|
||||
}
|
||||
const normalized = normalizeOptionalAccountId(decoded);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveMatrixEnvAccountToken(normalized) === token ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function listMatrixEnvAccountIds(env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
const ids = new Set<string>();
|
||||
for (const key of MATRIX_GLOBAL_ENV_KEYS) {
|
||||
if (typeof env[key] === "string" && env[key]?.trim()) {
|
||||
ids.add(normalizeAccountId("default"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(env)) {
|
||||
const match = MATRIX_SCOPED_ENV_RE.exec(key);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const accountId = decodeMatrixEnvAccountToken(match[1]);
|
||||
if (accountId) {
|
||||
ids.add(accountId);
|
||||
}
|
||||
}
|
||||
return Array.from(ids).toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
93
extensions/matrix/src/storage-paths.ts
Normal file
93
extensions/matrix/src/storage-paths.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
|
||||
export function sanitizeMatrixPathSegment(value: string): string {
|
||||
const cleaned = value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9._-]+/g, "_")
|
||||
.replace(/^_+|_+$/g, "");
|
||||
return cleaned || "unknown";
|
||||
}
|
||||
|
||||
export function resolveMatrixHomeserverKey(homeserver: string): string {
|
||||
try {
|
||||
const url = new URL(homeserver);
|
||||
if (url.host) {
|
||||
return sanitizeMatrixPathSegment(url.host);
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
return sanitizeMatrixPathSegment(homeserver);
|
||||
}
|
||||
|
||||
export function hashMatrixAccessToken(accessToken: string): string {
|
||||
return crypto.createHash("sha256").update(accessToken).digest("hex").slice(0, 16);
|
||||
}
|
||||
|
||||
export function resolveMatrixCredentialsFilename(accountId?: string | null): string {
|
||||
const normalized = normalizeAccountId(accountId);
|
||||
return normalized === DEFAULT_ACCOUNT_ID ? "credentials.json" : `credentials-${normalized}.json`;
|
||||
}
|
||||
|
||||
export function resolveMatrixCredentialsDir(stateDir: string): string {
|
||||
return path.join(stateDir, "credentials", "matrix");
|
||||
}
|
||||
|
||||
export function resolveMatrixCredentialsPath(params: {
|
||||
stateDir: string;
|
||||
accountId?: string | null;
|
||||
}): string {
|
||||
return path.join(
|
||||
resolveMatrixCredentialsDir(params.stateDir),
|
||||
resolveMatrixCredentialsFilename(params.accountId),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveMatrixLegacyFlatStoreRoot(stateDir: string): string {
|
||||
return path.join(stateDir, "matrix");
|
||||
}
|
||||
|
||||
export function resolveMatrixLegacyFlatStoragePaths(stateDir: string): {
|
||||
rootDir: string;
|
||||
storagePath: string;
|
||||
cryptoPath: string;
|
||||
} {
|
||||
const rootDir = resolveMatrixLegacyFlatStoreRoot(stateDir);
|
||||
return {
|
||||
rootDir,
|
||||
storagePath: path.join(rootDir, "bot-storage.json"),
|
||||
cryptoPath: path.join(rootDir, "crypto"),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveMatrixAccountStorageRoot(params: {
|
||||
stateDir: string;
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
accountId?: string | null;
|
||||
}): {
|
||||
rootDir: string;
|
||||
accountKey: string;
|
||||
tokenHash: string;
|
||||
} {
|
||||
const accountKey = sanitizeMatrixPathSegment(params.accountId ?? DEFAULT_ACCOUNT_ID);
|
||||
const userKey = sanitizeMatrixPathSegment(params.userId);
|
||||
const serverKey = resolveMatrixHomeserverKey(params.homeserver);
|
||||
const tokenHash = hashMatrixAccessToken(params.accessToken);
|
||||
return {
|
||||
rootDir: path.join(
|
||||
params.stateDir,
|
||||
"matrix",
|
||||
"accounts",
|
||||
accountKey,
|
||||
`${serverKey}__${userKey}`,
|
||||
tokenHash,
|
||||
),
|
||||
accountKey,
|
||||
tokenHash,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user