mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 23:04:26 +00:00
Matrix-js: remove register mode and require existing accounts
This commit is contained in:
@@ -70,7 +70,6 @@ function buildMatrixConfigUpdate(
|
||||
userId?: string;
|
||||
accessToken?: string;
|
||||
password?: string;
|
||||
register?: boolean;
|
||||
deviceName?: string;
|
||||
initialSyncLimit?: number;
|
||||
},
|
||||
@@ -93,7 +92,6 @@ function buildMatrixConfigUpdate(
|
||||
...(input.userId ? { userId: input.userId } : {}),
|
||||
...(input.accessToken ? { accessToken: input.accessToken } : {}),
|
||||
...(input.password ? { password: input.password } : {}),
|
||||
...(typeof input.register === "boolean" ? { register: input.register } : {}),
|
||||
...(input.deviceName ? { deviceName: input.deviceName } : {}),
|
||||
...(typeof input.initialSyncLimit === "number"
|
||||
? { initialSyncLimit: input.initialSyncLimit }
|
||||
@@ -115,7 +113,6 @@ function buildMatrixConfigUpdate(
|
||||
...(input.userId ? { userId: input.userId } : {}),
|
||||
...(input.accessToken ? { accessToken: input.accessToken } : {}),
|
||||
...(input.password ? { password: input.password } : {}),
|
||||
...(typeof input.register === "boolean" ? { register: input.register } : {}),
|
||||
...(input.deviceName ? { deviceName: input.deviceName } : {}),
|
||||
...(typeof input.initialSyncLimit === "number"
|
||||
? { initialSyncLimit: input.initialSyncLimit }
|
||||
@@ -168,7 +165,6 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
||||
"userId",
|
||||
"accessToken",
|
||||
"password",
|
||||
"register",
|
||||
"deviceName",
|
||||
"initialSyncLimit",
|
||||
],
|
||||
|
||||
@@ -161,8 +161,6 @@ describe("matrix-js CLI verification commands", () => {
|
||||
"@ops:example.org",
|
||||
"--password",
|
||||
"secret",
|
||||
"--register",
|
||||
"on",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
@@ -184,7 +182,6 @@ describe("matrix-js CLI verification commands", () => {
|
||||
accounts: {
|
||||
ops: expect.objectContaining({
|
||||
homeserver: "https://matrix.example.org",
|
||||
register: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -192,7 +189,6 @@ describe("matrix-js CLI verification commands", () => {
|
||||
}),
|
||||
);
|
||||
expect(console.log).toHaveBeenCalledWith("Saved matrix-js account: ops");
|
||||
expect(console.log).toHaveBeenCalledWith("Register mode: on");
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
"Bind this account to an agent: openclaw agents bind --agent <id> --bind matrix-js:ops",
|
||||
);
|
||||
|
||||
@@ -76,64 +76,9 @@ function parseOptionalInt(value: string | undefined, fieldName: string): number
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseToggle(value: string | undefined, fieldName: string): boolean | undefined {
|
||||
const trimmed = value?.trim().toLowerCase();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
if (["on", "true", "1", "yes"].includes(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (["off", "false", "0", "no"].includes(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
throw new Error(`${fieldName} must be on|off`);
|
||||
}
|
||||
|
||||
function applyRegisterFlag(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
register: boolean | undefined,
|
||||
): CoreConfig {
|
||||
if (typeof register !== "boolean") {
|
||||
return cfg;
|
||||
}
|
||||
const matrix = cfg.channels?.["matrix-js"] ?? {};
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...(cfg.channels ?? {}),
|
||||
"matrix-js": {
|
||||
...matrix,
|
||||
register,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const account = matrix.accounts?.[accountId] ?? {};
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...(cfg.channels ?? {}),
|
||||
"matrix-js": {
|
||||
...matrix,
|
||||
accounts: {
|
||||
...matrix.accounts,
|
||||
[accountId]: {
|
||||
...account,
|
||||
register,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type MatrixCliAccountAddResult = {
|
||||
accountId: string;
|
||||
configPath: string;
|
||||
registerMode: boolean | undefined;
|
||||
useEnv: boolean;
|
||||
};
|
||||
|
||||
@@ -147,12 +92,10 @@ async function addMatrixJsAccount(params: {
|
||||
deviceName?: string;
|
||||
initialSyncLimit?: string;
|
||||
useEnv?: boolean;
|
||||
register?: string;
|
||||
}): Promise<MatrixCliAccountAddResult> {
|
||||
const runtime = getMatrixRuntime();
|
||||
const cfg = runtime.config.loadConfig() as CoreConfig;
|
||||
const accountId = normalizeAccountId(params.account);
|
||||
const registerMode = parseToggle(params.register, "--register");
|
||||
const setup = matrixPlugin.setup;
|
||||
if (!setup?.applyAccountConfig) {
|
||||
throw new Error("Matrix-js account setup is unavailable.");
|
||||
@@ -183,8 +126,7 @@ async function addMatrixJsAccount(params: {
|
||||
accountId,
|
||||
input,
|
||||
}) as CoreConfig;
|
||||
const next = applyRegisterFlag(updated, accountId, registerMode);
|
||||
await runtime.config.writeConfigFile(next as never);
|
||||
await runtime.config.writeConfigFile(updated as never);
|
||||
|
||||
return {
|
||||
accountId,
|
||||
@@ -192,7 +134,6 @@ async function addMatrixJsAccount(params: {
|
||||
accountId === DEFAULT_ACCOUNT_ID
|
||||
? "channels.matrix-js"
|
||||
: `channels.matrix-js.accounts.${accountId}`,
|
||||
registerMode,
|
||||
useEnv: input.useEnv === true,
|
||||
};
|
||||
}
|
||||
@@ -506,7 +447,6 @@ export function registerMatrixJsCli(params: { program: Command }): void {
|
||||
.option("--device-name <name>", "Matrix device display name")
|
||||
.option("--initial-sync-limit <n>", "Matrix initial sync limit")
|
||||
.option("--use-env", "Use MATRIX_* env vars (default account only)")
|
||||
.option("--register <on|off>", "Enable/disable register mode for password auth")
|
||||
.option("--verbose", "Show setup details")
|
||||
.option("--json", "Output as JSON")
|
||||
.action(
|
||||
@@ -520,7 +460,6 @@ export function registerMatrixJsCli(params: { program: Command }): void {
|
||||
deviceName?: string;
|
||||
initialSyncLimit?: string;
|
||||
useEnv?: boolean;
|
||||
register?: string;
|
||||
verbose?: boolean;
|
||||
json?: boolean;
|
||||
}) => {
|
||||
@@ -538,14 +477,10 @@ export function registerMatrixJsCli(params: { program: Command }): void {
|
||||
deviceName: options.deviceName,
|
||||
initialSyncLimit: options.initialSyncLimit,
|
||||
useEnv: options.useEnv === true,
|
||||
register: options.register,
|
||||
}),
|
||||
onText: (result) => {
|
||||
console.log(`Saved matrix-js account: ${result.accountId}`);
|
||||
console.log(`Config path: ${result.configPath}`);
|
||||
if (typeof result.registerMode === "boolean") {
|
||||
console.log(`Register mode: ${result.registerMode ? "on" : "off"}`);
|
||||
}
|
||||
console.log(
|
||||
`Credentials source: ${result.useEnv ? "MATRIX_* env vars" : "inline config"}`,
|
||||
);
|
||||
|
||||
@@ -43,7 +43,6 @@ export const MatrixConfigSchema = z.object({
|
||||
userId: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
register: z.boolean().optional(),
|
||||
deviceId: z.string().optional(),
|
||||
deviceName: z.string().optional(),
|
||||
initialSyncLimit: z.number().optional(),
|
||||
|
||||
@@ -4,9 +4,7 @@ import { resolveMatrixAuth, resolveMatrixConfig } from "./client.js";
|
||||
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);
|
||||
const saveMatrixCredentialsMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./credentials.js", () => ({
|
||||
loadMatrixCredentials: vi.fn(() => null),
|
||||
@@ -15,12 +13,6 @@ vi.mock("./credentials.js", () => ({
|
||||
touchMatrixCredentials: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./client/register-mode.js", () => ({
|
||||
prepareMatrixRegisterMode: prepareMatrixRegisterModeMock,
|
||||
finalizeMatrixRegisterConfigAfterSuccess: finalizeMatrixRegisterConfigAfterSuccessMock,
|
||||
resetPreparedMatrixRegisterModesForTests: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("resolveMatrixConfig", () => {
|
||||
it("prefers config over env", () => {
|
||||
const cfg = {
|
||||
@@ -48,7 +40,6 @@ describe("resolveMatrixConfig", () => {
|
||||
userId: "@cfg:example.org",
|
||||
accessToken: "cfg-token",
|
||||
password: "cfg-pass",
|
||||
register: false,
|
||||
deviceId: undefined,
|
||||
deviceName: "CfgDevice",
|
||||
initialSyncLimit: 5,
|
||||
@@ -71,32 +62,11 @@ describe("resolveMatrixConfig", () => {
|
||||
expect(resolved.userId).toBe("@env:example.org");
|
||||
expect(resolved.accessToken).toBe("env-token");
|
||||
expect(resolved.password).toBe("env-pass");
|
||||
expect(resolved.register).toBe(false);
|
||||
expect(resolved.deviceId).toBe("ENVDEVICE");
|
||||
expect(resolved.deviceName).toBe("EnvDevice");
|
||||
expect(resolved.initialSyncLimit).toBeUndefined();
|
||||
expect(resolved.encryption).toBe(false);
|
||||
});
|
||||
|
||||
it("reads register flag from config and env", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
"matrix-js": {
|
||||
register: true,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
const resolvedFromCfg = resolveMatrixConfig(cfg, {} as NodeJS.ProcessEnv);
|
||||
expect(resolvedFromCfg.register).toBe(true);
|
||||
|
||||
const resolvedFromEnv = resolveMatrixConfig(
|
||||
{} as CoreConfig,
|
||||
{
|
||||
MATRIX_REGISTER: "1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
);
|
||||
expect(resolvedFromEnv.register).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMatrixAuth", () => {
|
||||
@@ -104,8 +74,6 @@ 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 () => {
|
||||
@@ -158,88 +126,9 @@ describe("resolveMatrixAuth", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("can register account when password login fails and register mode is enabled", async () => {
|
||||
it("surfaces password login errors when account credentials are invalid", async () => {
|
||||
const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest");
|
||||
doRequestSpy
|
||||
.mockRejectedValueOnce(new Error("Invalid username or password"))
|
||||
.mockResolvedValueOnce({
|
||||
access_token: "tok-registered",
|
||||
user_id: "@newbot:example.org",
|
||||
device_id: "REGDEVICE123",
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
channels: {
|
||||
"matrix-js": {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@newbot:example.org",
|
||||
password: "secret",
|
||||
register: true,
|
||||
encryption: true,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
const auth = await resolveMatrixAuth({
|
||||
cfg,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(doRequestSpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"POST",
|
||||
"/_matrix/client/v3/login",
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
type: "m.login.password",
|
||||
device_id: undefined,
|
||||
}),
|
||||
);
|
||||
expect(doRequestSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"POST",
|
||||
"/_matrix/client/v3/register",
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
username: "newbot",
|
||||
auth: { type: "m.login.dummy" },
|
||||
}),
|
||||
);
|
||||
expect(auth).toMatchObject({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@newbot:example.org",
|
||||
accessToken: "tok-registered",
|
||||
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-js.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",
|
||||
});
|
||||
doRequestSpy.mockRejectedValueOnce(new Error("Invalid username or password"));
|
||||
|
||||
const cfg = {
|
||||
channels: {
|
||||
@@ -247,12 +136,16 @@ describe("resolveMatrixAuth", () => {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
password: "secret",
|
||||
register: true,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
const auth = await resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv });
|
||||
await expect(
|
||||
resolveMatrixAuth({
|
||||
cfg,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).rejects.toThrow("Invalid username or password");
|
||||
|
||||
expect(doRequestSpy).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
@@ -262,44 +155,41 @@ describe("resolveMatrixAuth", () => {
|
||||
type: "m.login.password",
|
||||
}),
|
||||
);
|
||||
expect(auth.accessToken).toBe("tok-123");
|
||||
expect(prepareMatrixRegisterModeMock).toHaveBeenCalledTimes(1);
|
||||
expect(saveMatrixCredentialsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires matrix-js.password when matrix-js.register=true", async () => {
|
||||
it("uses cached matching credentials when access token is not configured", 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 cfg = {
|
||||
channels: {
|
||||
"matrix-js": {
|
||||
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-js.register=true",
|
||||
);
|
||||
expect(prepareMatrixRegisterModeMock).not.toHaveBeenCalled();
|
||||
expect(finalizeMatrixRegisterConfigAfterSuccessMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires matrix-js.userId when matrix-js.register=true", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
"matrix-js": {
|
||||
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-js.register=true",
|
||||
);
|
||||
expect(prepareMatrixRegisterModeMock).not.toHaveBeenCalled();
|
||||
expect(finalizeMatrixRegisterConfigAfterSuccessMock).not.toHaveBeenCalled();
|
||||
const auth = await resolveMatrixAuth({
|
||||
cfg,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(auth).toMatchObject({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "cached-token",
|
||||
deviceId: "CACHEDDEVICE",
|
||||
});
|
||||
expect(saveMatrixCredentialsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to config deviceId when cached credentials are missing it", async () => {
|
||||
|
||||
@@ -3,36 +3,12 @@ import { getMatrixRuntime } from "../../runtime.js";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import { MatrixClient } from "../sdk.js";
|
||||
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
||||
import {
|
||||
finalizeMatrixRegisterConfigAfterSuccess,
|
||||
prepareMatrixRegisterMode,
|
||||
} from "./register-mode.js";
|
||||
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
||||
|
||||
function clean(value?: string): string {
|
||||
return value?.trim() ?? "";
|
||||
}
|
||||
|
||||
function parseOptionalBoolean(value: unknown): boolean | undefined {
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
if (["1", "true", "yes", "on"].includes(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (["0", "false", "no", "off"].includes(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findAccountConfig(cfg: CoreConfig, accountId: string): Record<string, unknown> {
|
||||
const accounts = cfg.channels?.["matrix-js"]?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
@@ -54,70 +30,6 @@ function findAccountConfig(cfg: CoreConfig, accountId: string): Record<string, u
|
||||
return {};
|
||||
}
|
||||
|
||||
function resolveMatrixLocalpart(userId: string): string {
|
||||
const trimmed = userId.trim();
|
||||
const noPrefix = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
||||
const localpart = noPrefix.split(":")[0]?.trim() || "";
|
||||
if (!localpart) {
|
||||
throw new Error(`Invalid Matrix userId for registration: ${userId}`);
|
||||
}
|
||||
return localpart;
|
||||
}
|
||||
|
||||
async function registerMatrixPasswordAccount(params: {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
password: string;
|
||||
deviceId?: string;
|
||||
deviceName?: string;
|
||||
}): Promise<{
|
||||
access_token?: string;
|
||||
user_id?: string;
|
||||
device_id?: string;
|
||||
}> {
|
||||
const registerClient = new MatrixClient(params.homeserver, "");
|
||||
const payload = {
|
||||
username: resolveMatrixLocalpart(params.userId),
|
||||
password: params.password,
|
||||
inhibit_login: false,
|
||||
device_id: params.deviceId,
|
||||
initial_device_display_name: params.deviceName ?? "OpenClaw Gateway",
|
||||
};
|
||||
|
||||
let firstError: unknown = null;
|
||||
try {
|
||||
return (await registerClient.doRequest("POST", "/_matrix/client/v3/register", undefined, {
|
||||
...payload,
|
||||
auth: { type: "m.login.dummy" },
|
||||
})) as {
|
||||
access_token?: string;
|
||||
user_id?: string;
|
||||
device_id?: string;
|
||||
};
|
||||
} catch (err) {
|
||||
firstError = err;
|
||||
}
|
||||
|
||||
try {
|
||||
return (await registerClient.doRequest(
|
||||
"POST",
|
||||
"/_matrix/client/v3/register",
|
||||
undefined,
|
||||
payload,
|
||||
)) as {
|
||||
access_token?: string;
|
||||
user_id?: string;
|
||||
device_id?: string;
|
||||
};
|
||||
} catch (err) {
|
||||
const firstMessage = firstError instanceof Error ? firstError.message : String(firstError);
|
||||
const secondMessage = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(
|
||||
`Matrix registration failed (dummy auth: ${firstMessage}; plain registration: ${secondMessage})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveMatrixConfig(
|
||||
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
@@ -127,8 +39,6 @@ export function resolveMatrixConfig(
|
||||
const userId = clean(matrix.userId) || clean(env.MATRIX_USER_ID);
|
||||
const accessToken = clean(matrix.accessToken) || clean(env.MATRIX_ACCESS_TOKEN) || undefined;
|
||||
const password = clean(matrix.password) || clean(env.MATRIX_PASSWORD) || undefined;
|
||||
const register =
|
||||
parseOptionalBoolean(matrix.register) ?? parseOptionalBoolean(env.MATRIX_REGISTER) ?? false;
|
||||
const deviceId = clean(matrix.deviceId) || clean(env.MATRIX_DEVICE_ID) || undefined;
|
||||
const deviceName = clean(matrix.deviceName) || clean(env.MATRIX_DEVICE_NAME) || undefined;
|
||||
const initialSyncLimit =
|
||||
@@ -141,7 +51,6 @@ export function resolveMatrixConfig(
|
||||
userId,
|
||||
accessToken,
|
||||
password,
|
||||
register,
|
||||
deviceId,
|
||||
deviceName,
|
||||
initialSyncLimit,
|
||||
@@ -180,11 +89,6 @@ export function resolveMatrixConfigForAccount(
|
||||
accountAccessToken || clean(matrix.accessToken) || clean(env.MATRIX_ACCESS_TOKEN) || undefined;
|
||||
const password =
|
||||
accountPassword || clean(matrix.password) || clean(env.MATRIX_PASSWORD) || undefined;
|
||||
const register =
|
||||
parseOptionalBoolean(account.register) ??
|
||||
parseOptionalBoolean(matrix.register) ??
|
||||
parseOptionalBoolean(env.MATRIX_REGISTER) ??
|
||||
false;
|
||||
const deviceId =
|
||||
accountDeviceId || clean(matrix.deviceId) || clean(env.MATRIX_DEVICE_ID) || undefined;
|
||||
const deviceName =
|
||||
@@ -207,7 +111,6 @@ export function resolveMatrixConfigForAccount(
|
||||
userId,
|
||||
accessToken,
|
||||
password,
|
||||
register,
|
||||
deviceId,
|
||||
deviceName,
|
||||
initialSyncLimit,
|
||||
@@ -226,7 +129,6 @@ export async function resolveMatrixAuth(params?: {
|
||||
const resolved = accountId
|
||||
? resolveMatrixConfigForAccount(cfg, accountId, env)
|
||||
: resolveMatrixConfig(cfg, env);
|
||||
const registerFromConfig = resolved.register === true;
|
||||
if (!resolved.homeserver) {
|
||||
throw new Error("Matrix homeserver is required (matrix-js.homeserver)");
|
||||
}
|
||||
@@ -248,23 +150,8 @@ export async function resolveMatrixAuth(params?: {
|
||||
? cached
|
||||
: null;
|
||||
|
||||
if (registerFromConfig) {
|
||||
if (!resolved.userId) {
|
||||
throw new Error("Matrix userId is required when matrix-js.register=true");
|
||||
}
|
||||
if (!resolved.password) {
|
||||
throw new Error("Matrix password is required when matrix-js.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 && !registerFromConfig) {
|
||||
if (resolved.accessToken) {
|
||||
let userId = resolved.userId;
|
||||
const hasMatchingCachedToken = cachedCredentials?.accessToken === resolved.accessToken;
|
||||
let knownDeviceId = hasMatchingCachedToken
|
||||
@@ -322,7 +209,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
};
|
||||
}
|
||||
|
||||
if (cachedCredentials && !registerFromConfig) {
|
||||
if (cachedCredentials) {
|
||||
touchMatrixCredentials(env, accountId);
|
||||
return {
|
||||
homeserver: cachedCredentials.homeserver,
|
||||
@@ -351,48 +238,21 @@ export async function resolveMatrixAuth(params?: {
|
||||
// Login with password using the same hardened request path as other Matrix HTTP calls.
|
||||
ensureMatrixSdkLoggingConfigured();
|
||||
const loginClient = new MatrixClient(resolved.homeserver, "");
|
||||
let login: {
|
||||
const login = (await loginClient.doRequest("POST", "/_matrix/client/v3/login", undefined, {
|
||||
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;
|
||||
user_id?: string;
|
||||
device_id?: string;
|
||||
};
|
||||
try {
|
||||
login = (await loginClient.doRequest("POST", "/_matrix/client/v3/login", undefined, {
|
||||
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;
|
||||
user_id?: string;
|
||||
device_id?: string;
|
||||
};
|
||||
} catch (loginErr) {
|
||||
if (!resolved.register) {
|
||||
throw loginErr;
|
||||
}
|
||||
try {
|
||||
login = await registerMatrixPasswordAccount({
|
||||
homeserver: resolved.homeserver,
|
||||
userId: resolved.userId,
|
||||
password: resolved.password,
|
||||
deviceId: resolved.deviceId,
|
||||
deviceName: resolved.deviceName,
|
||||
});
|
||||
} catch (registerErr) {
|
||||
const loginMessage = loginErr instanceof Error ? loginErr.message : String(loginErr);
|
||||
const registerMessage =
|
||||
registerErr instanceof Error ? registerErr.message : String(registerErr);
|
||||
throw new Error(
|
||||
`Matrix login failed (${loginMessage}) and account registration failed (${registerMessage})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const accessToken = login.access_token?.trim();
|
||||
if (!accessToken) {
|
||||
throw new Error("Matrix login/registration did not return an access token");
|
||||
throw new Error("Matrix login did not return an access token");
|
||||
}
|
||||
|
||||
const auth: MatrixAuth = {
|
||||
@@ -417,13 +277,5 @@ export async function resolveMatrixAuth(params?: {
|
||||
accountId,
|
||||
);
|
||||
|
||||
if (registerFromConfig) {
|
||||
await finalizeMatrixRegisterConfigAfterSuccess({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
deviceId: auth.deviceId,
|
||||
});
|
||||
}
|
||||
|
||||
return auth;
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import * as runtimeModule from "../../runtime.js";
|
||||
import type { CoreConfig } from "../../types.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-js");
|
||||
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-js": {
|
||||
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-js": {
|
||||
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-js": expect.objectContaining({
|
||||
register: false,
|
||||
homeserver: "https://matrix.gumadeiras.com",
|
||||
userId: "@pinguini:matrix.gumadeiras.com",
|
||||
deviceId: "DEVICE123",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const firstCall = (writeConfigFile.mock.calls as unknown[][])[0];
|
||||
const written = (firstCall?.[0] ?? {}) as CoreConfig;
|
||||
expect(written.channels?.["matrix-js"]?.accessToken).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,125 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
import type { CoreConfig } from "../../types.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-js"] ?? {};
|
||||
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-js"]?.register !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matrixCfg = cfg.channels?.["matrix-js"] ?? {};
|
||||
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-js": nextMatrix as NonNullable<CoreConfig["channels"]>["matrix-js"],
|
||||
},
|
||||
};
|
||||
|
||||
await runtime.config.writeConfigFile(next as never);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resetPreparedMatrixRegisterModesForTests(): void {
|
||||
preparedRegisterKeys.clear();
|
||||
}
|
||||
@@ -4,7 +4,6 @@ export type MatrixResolvedConfig = {
|
||||
accessToken?: string;
|
||||
deviceId?: string;
|
||||
password?: string;
|
||||
register?: boolean;
|
||||
deviceName?: string;
|
||||
initialSyncLimit?: number;
|
||||
encryption?: boolean;
|
||||
|
||||
@@ -41,9 +41,8 @@ async function noteMatrixAuthHelp(prompter: WizardPrompter): Promise<void> {
|
||||
await prompter.note(
|
||||
[
|
||||
"Matrix requires a homeserver URL.",
|
||||
"Use an access token (recommended), password login, or account registration.",
|
||||
"Use an access token (recommended) or password login to an existing account.",
|
||||
"With access token: user ID is fetched automatically.",
|
||||
"Password + register mode can create an account on homeservers with open registration.",
|
||||
"Env vars supported: MATRIX_HOMESERVER, MATRIX_USER_ID, MATRIX_ACCESS_TOKEN, MATRIX_PASSWORD.",
|
||||
`Docs: ${formatDocsLink("/channels/matrix", "channels/matrix")}`,
|
||||
].join("\n"),
|
||||
@@ -266,7 +265,6 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
let accessToken = existing.accessToken ?? "";
|
||||
let password = existing.password ?? "";
|
||||
let userId = existing.userId ?? "";
|
||||
let register = existing.register === true;
|
||||
|
||||
if (accessToken || password) {
|
||||
const keep = await prompter.confirm({
|
||||
@@ -277,7 +275,6 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
accessToken = "";
|
||||
password = "";
|
||||
userId = "";
|
||||
register = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,10 +285,6 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
options: [
|
||||
{ value: "token", label: "Access token (user ID fetched automatically)" },
|
||||
{ value: "password", label: "Password (requires user ID)" },
|
||||
{
|
||||
value: "register",
|
||||
label: "Register account (open homeserver registration required)",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -305,9 +298,8 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
// With access token, we can fetch the userId automatically - don't prompt for it
|
||||
// The client.ts will use whoami() to get it
|
||||
userId = "";
|
||||
register = false;
|
||||
} else {
|
||||
// Password auth and registration mode require user ID upfront
|
||||
// Password auth requires user ID upfront.
|
||||
userId = String(
|
||||
await prompter.text({
|
||||
message: "Matrix user ID",
|
||||
@@ -333,7 +325,6 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
register = authMode === "register";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +352,6 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
userId: userId || undefined,
|
||||
accessToken: accessToken || undefined,
|
||||
password: password || undefined,
|
||||
register,
|
||||
deviceName: deviceName || undefined,
|
||||
encryption: enableEncryption || undefined,
|
||||
},
|
||||
|
||||
@@ -58,8 +58,6 @@ export type MatrixConfig = {
|
||||
accessToken?: string;
|
||||
/** Matrix password (used only to fetch access token). */
|
||||
password?: string;
|
||||
/** Auto-register account when password login fails (open registration homeservers). */
|
||||
register?: boolean;
|
||||
/** Optional Matrix device id (recommended when using access tokens + E2EE). */
|
||||
deviceId?: string;
|
||||
/** Optional device name when logging in via password. */
|
||||
|
||||
Reference in New Issue
Block a user