mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 13:22:14 +00:00
fix(matrix): keep cached env accounts discoverable
This commit is contained in:
@@ -658,6 +658,7 @@ See [Pairing](/channels/pairing) for the shared DM pairing flow and storage layo
|
||||
Top-level `channels.matrix` values act as defaults for named accounts unless an account overrides them.
|
||||
You can scope inherited room entries to one Matrix account with `groups.<room>.account` (or legacy `rooms.<room>.account`).
|
||||
Entries without `account` stay shared across all Matrix accounts, and entries with `account: "default"` still work when the default account is configured directly on top-level `channels.matrix.*`.
|
||||
Partial shared auth defaults do not create a separate implicit default account by themselves. OpenClaw only treats Matrix accounts with a usable homeserver plus access-token or user-ID-based auth shape as selectable for implicit routing.
|
||||
Set `defaultAccount` when you want OpenClaw to prefer one named Matrix account for implicit routing, probing, and CLI operations.
|
||||
If you configure multiple named accounts, set `defaultAccount` or pass `--account <id>` for CLI commands that rely on implicit account selection.
|
||||
Pass `--account <id>` to `openclaw matrix verify ...` and `openclaw matrix devices ...` when you want to override that implicit selection for one command.
|
||||
|
||||
@@ -83,4 +83,21 @@ describe("Matrix account selection topology", () => {
|
||||
expect(resolveMatrixDefaultOrOnlyAccountId(cfg, env)).toBe("ops");
|
||||
expect(requiresExplicitMatrixDefaultAccount(cfg, env)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps env-backed named accounts that rely on cached credentials", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
const env = {
|
||||
MATRIX_OPS_USER_ID: "@ops:example.org",
|
||||
} as NodeJS.ProcessEnv;
|
||||
|
||||
expect(resolveConfiguredMatrixAccountIds(cfg, env)).toEqual(["ops"]);
|
||||
expect(resolveMatrixDefaultOrOnlyAccountId(cfg, env)).toBe("ops");
|
||||
expect(requiresExplicitMatrixDefaultAccount(cfg, env)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,13 +78,15 @@ function resolveGlobalMatrixEnvStringSources(env: NodeJS.ProcessEnv): MatrixTopo
|
||||
};
|
||||
}
|
||||
|
||||
function hasReadyResolvedMatrixAuth(values: {
|
||||
function hasUsableResolvedMatrixAuth(values: {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
password: string;
|
||||
}): boolean {
|
||||
return Boolean(values.homeserver && (values.accessToken || (values.userId && values.password)));
|
||||
// Account discovery must keep homeserver+userId shapes because auth can still
|
||||
// resolve through cached Matrix credentials even when no fresh token/password
|
||||
// is present in config or env.
|
||||
return Boolean(values.homeserver && (values.accessToken || values.userId));
|
||||
}
|
||||
|
||||
function hasReadyEffectiveMatrixAccountSource(params: {
|
||||
@@ -99,7 +101,7 @@ function hasReadyEffectiveMatrixAccountSource(params: {
|
||||
channel: resolveMatrixChannelStringSources(params.channel),
|
||||
globalEnv: resolveGlobalMatrixEnvStringSources(params.env),
|
||||
});
|
||||
return hasReadyResolvedMatrixAuth(resolved);
|
||||
return hasUsableResolvedMatrixAuth(resolved);
|
||||
}
|
||||
|
||||
function hasConfiguredDefaultMatrixAccountSource(params: {
|
||||
|
||||
@@ -451,6 +451,22 @@ describe("resolveMatrixConfig", () => {
|
||||
expect(resolveMatrixAuthContext({ cfg, env }).accountId).toBe("ops");
|
||||
});
|
||||
|
||||
it("keeps implicit selection for env-backed accounts that can use cached credentials", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
const env = {
|
||||
MATRIX_OPS_USER_ID: "@ops:example.org",
|
||||
} as NodeJS.ProcessEnv;
|
||||
|
||||
expect(resolveImplicitMatrixAccountId(cfg, env)).toBe("ops");
|
||||
expect(resolveMatrixAuthContext({ cfg, env }).accountId).toBe("ops");
|
||||
});
|
||||
|
||||
it("rejects explicit non-default account ids that are neither configured nor scoped in env", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
@@ -800,6 +816,43 @@ describe("resolveMatrixAuth", () => {
|
||||
expect(saveMatrixCredentialsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses cached matching credentials for env-backed named accounts without fresh auth", async () => {
|
||||
vi.mocked(credentialsReadModule!.loadMatrixCredentials).mockReturnValue({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@ops:example.org",
|
||||
accessToken: "cached-token",
|
||||
deviceId: "CACHEDDEVICE",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
vi.mocked(credentialsReadModule!.credentialsMatchConfig).mockReturnValue(true);
|
||||
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
const env = {
|
||||
MATRIX_OPS_USER_ID: "@ops:example.org",
|
||||
} as NodeJS.ProcessEnv;
|
||||
|
||||
const auth = await resolveMatrixAuth({
|
||||
cfg,
|
||||
env,
|
||||
accountId: "ops",
|
||||
});
|
||||
|
||||
expect(auth).toMatchObject({
|
||||
accountId: "ops",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@ops:example.org",
|
||||
accessToken: "cached-token",
|
||||
deviceId: "CACHEDDEVICE",
|
||||
});
|
||||
expect(saveMatrixCredentialsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects embedded credentials in Matrix homeserver URLs", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
||||
@@ -148,6 +148,40 @@ describe("matrix credentials storage", () => {
|
||||
expect(fs.existsSync(currentPath)).toBe(false);
|
||||
});
|
||||
|
||||
it("migrates legacy credentials to the named account when top-level auth is only a shared default", () => {
|
||||
const stateDir = setupStateDir({
|
||||
channels: {
|
||||
matrix: {
|
||||
accessToken: "shared-token",
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "ops-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const legacyPath = path.join(stateDir, "credentials", "matrix", "credentials.json");
|
||||
const currentPath = resolveMatrixCredentialsPath({}, "ops");
|
||||
fs.mkdirSync(path.dirname(legacyPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
legacyPath,
|
||||
JSON.stringify({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@ops:example.org",
|
||||
accessToken: "legacy-token",
|
||||
createdAt: "2026-03-01T10:00:00.000Z",
|
||||
}),
|
||||
);
|
||||
|
||||
const loaded = loadMatrixCredentials({}, "ops");
|
||||
|
||||
expect(loaded?.accessToken).toBe("legacy-token");
|
||||
expect(fs.existsSync(legacyPath)).toBe(false);
|
||||
expect(fs.existsSync(currentPath)).toBe(true);
|
||||
});
|
||||
|
||||
it("clears both current and legacy credential paths", () => {
|
||||
const stateDir = setupStateDir({
|
||||
channels: {
|
||||
|
||||
Reference in New Issue
Block a user