Skip to content

Commit 49db0df

Browse files
authored
feat(matrix): optional password login alongside access token (#14)
## Added - Matrix can authenticate with `MATRIX_USER` and `MATRIX_PASSWORD` when no access token is available; token auth remains supported. ## Changed - Helm: optional `config.matrix.user` and `secrets.matrixPassword` for password-based installs. ## Fixed - Use `client.loginRequest` instead of deprecated `client.login` for password flows.
1 parent a7a12ef commit 49db0df

9 files changed

Lines changed: 187 additions & 26 deletions

File tree

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ Kaytoo is configured via environment variables. For a minimal example, see `.env
101101
**Chat adapters** - configure at least one to run in chat mode. Kaytoo posts scheduled insights to every configured platform and answers chat replies on the same platform a message arrives from. See the per-platform setup and full variable tables below:
102102

103103
- [Slack](#slack): `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, `SLACK_CHANNEL_ID`
104-
- [Matrix](#matrix): `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN`, `MATRIX_DEFAULT_ROOM_ID`
104+
- [Matrix](#matrix): `MATRIX_HOMESERVER`, `MATRIX_DEFAULT_ROOM_ID`, plus either `MATRIX_ACCESS_TOKEN` or `MATRIX_USER` + `MATRIX_PASSWORD`
105105
- [Mattermost](#mattermost): `MATTERMOST_URL`, `MATTERMOST_TOKEN`, `MATTERMOST_CHANNEL_ID`
106106

107107
**Search backend** (Choose either OpenSearch or Elasticsearch. Do not set both)
@@ -202,21 +202,27 @@ Uses [`matrix-js-sdk`](https://github.com/matrix-org/matrix-js-sdk) with an in-m
202202
| Variable | Required | Default | Notes |
203203
| --- | --- | --- | --- |
204204
| `MATRIX_HOMESERVER` | yes | - | Homeserver base URL. |
205-
| `MATRIX_ACCESS_TOKEN` | yes | - | Access token for the bot user. |
205+
| `MATRIX_ACCESS_TOKEN` | one-of | - | Long-lived access token for the bot user. Required if `MATRIX_USER`/`MATRIX_PASSWORD` are not set. |
206+
| `MATRIX_USER` | one-of | - | Username or full MXID for password login. Required together with `MATRIX_PASSWORD` if no access token is provided. |
207+
| `MATRIX_PASSWORD` | one-of | - | Password for `MATRIX_USER`. Kaytoo logs in via `matrix-js-sdk` using `m.login.password` at startup. |
206208
| `MATRIX_DEFAULT_ROOM_ID` | yes | - | Room for scheduled insight posts. Kaytoo also attempts to join it on startup. |
207209

210+
Provide **either** `MATRIX_ACCESS_TOKEN` **or** the `MATRIX_USER` + `MATRIX_PASSWORD` pair, not both. Token auth is preferred when the homeserver supports it.
211+
208212
##### Setup
209213

210214
1. **Create a dedicated Matrix user** for Kaytoo (recommended) on your homeserver.
211215
- If registration is closed, have a homeserver admin create the account.
212216
- Log in once with a Matrix client (for example Element) to confirm the account works.
213217

214-
2. **Create an access token** for that user.
215-
- In Element Web: open **Help & About** -> **Advanced** -> **Access Token** (wording varies by client).
218+
2. **Pick an authentication method:**
219+
- **Access token** (preferred when available): in Element Web open **Help & About** -> **Advanced** -> **Access Token** (wording varies by client).
220+
- **Username + password** (fallback for homeservers that do not expose a token UI): use the same credentials your bot logs in with.
216221

217222
3. **Set Kaytoo env vars**:
218223
- `MATRIX_HOMESERVER` should be the **client API base URL** for your homeserver (often `https://matrix.example.com`, not always the same host as your MXID server part).
219-
- `MATRIX_ACCESS_TOKEN` is the token from step 2.
224+
- For token auth: set `MATRIX_ACCESS_TOKEN` from step 2.
225+
- For password auth: set `MATRIX_USER` (localpart `kaytoo` or full MXID `@kaytoo:matrix.example.org`) and `MATRIX_PASSWORD`.
220226

221227
4. **Room membership**:
222228
- Invite the bot user into each room where Kaytoo should respond.

helm/kaytoo/templates/configmap.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ data:
3030
{{- if .Values.config.matrix.defaultRoomId }}
3131
MATRIX_DEFAULT_ROOM_ID: {{ .Values.config.matrix.defaultRoomId | quote }}
3232
{{- end }}
33+
{{- if .Values.config.matrix.user }}
34+
MATRIX_USER: {{ .Values.config.matrix.user | quote }}
35+
{{- end }}
3336

3437
{{- if .Values.config.mattermost.url }}
3538
MATTERMOST_URL: {{ .Values.config.mattermost.url | quote }}

helm/kaytoo/templates/secret.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ stringData:
1616
{{- if .Values.secrets.matrixAccessToken }}
1717
MATRIX_ACCESS_TOKEN: {{ .Values.secrets.matrixAccessToken | quote }}
1818
{{- end }}
19+
{{- if .Values.secrets.matrixPassword }}
20+
MATRIX_PASSWORD: {{ .Values.secrets.matrixPassword | quote }}
21+
{{- end }}
1922
{{- if .Values.secrets.mattermostToken }}
2023
MATTERMOST_TOKEN: {{ .Values.secrets.mattermostToken | quote }}
2124
{{- end }}

helm/kaytoo/values.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ config:
3333
matrix:
3434
homeserver: ""
3535
defaultRoomId: ""
36+
# Optional username for password login (alternative to secrets.matrixAccessToken).
37+
# Pair with secrets.matrixPassword. Accepts the localpart (`kaytoo`) or full MXID (`@kaytoo:example.org`).
38+
user: ""
3639
mattermost:
3740
url: ""
3841
channelId: ""
@@ -54,6 +57,8 @@ secrets:
5457
slackBotToken: ""
5558
slackAppToken: ""
5659
matrixAccessToken: ""
60+
# Set instead of matrixAccessToken when the homeserver only allows password login.
61+
matrixPassword: ""
5762
mattermostToken: ""
5863
opensearchUsername: ""
5964
opensearchPassword: ""

src/chat/adapters/matrix.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import type { ChatEvent } from '../types.js';
1313

1414
const log = getLogger({ component: 'chat.matrix' });
1515

16+
export type MatrixAuth = { accessToken: string } | { user: string; password: string };
17+
1618
async function whoamiUserId(baseUrl: string, accessToken: string): Promise<string | undefined> {
1719
const root = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
1820
const url = new URL('_matrix/client/v3/account/whoami', root);
@@ -22,25 +24,40 @@ async function whoamiUserId(baseUrl: string, accessToken: string): Promise<strin
2224
return typeof body.user_id === 'string' ? body.user_id : undefined;
2325
}
2426

27+
function loginIdentifier(user: string): { type: 'm.id.user'; user: string } {
28+
const localpart = user.startsWith('@') ? user.replace(/^@/, '').split(':')[0]! : user;
29+
return { type: 'm.id.user', user: localpart };
30+
}
31+
2532
export async function startMatrixAdapter(opts: {
2633
homeserverUrl: string;
27-
accessToken: string;
34+
auth: MatrixAuth;
2835
matrixSdkLevel: MatrixSdkLevel;
2936
defaultRoomId?: string;
3037
onEvent: (evt: ChatEvent) => Promise<void>;
3138
}): Promise<{ stop: () => Promise<void>; client: MatrixClient }> {
3239
const logger = createMatrixJsSdkLogger(opts.matrixSdkLevel);
33-
const userId = await whoamiUserId(opts.homeserverUrl, opts.accessToken);
40+
const userId =
41+
'accessToken' in opts.auth ? await whoamiUserId(opts.homeserverUrl, opts.auth.accessToken) : undefined;
3442

3543
const client = createClient({
3644
baseUrl: opts.homeserverUrl,
37-
accessToken: opts.accessToken,
45+
...('accessToken' in opts.auth ? { accessToken: opts.auth.accessToken } : {}),
3846
...(userId ? { userId } : {}),
3947
store: new MemoryStore(),
4048
logger,
4149
timelineSupport: true,
4250
});
4351

52+
if ('user' in opts.auth) {
53+
await client.loginRequest({
54+
type: 'm.login.password',
55+
identifier: loginIdentifier(opts.auth.user),
56+
password: opts.auth.password,
57+
initial_device_display_name: 'kaytoo',
58+
});
59+
}
60+
4461
const onTimeline = async (
4562
event: MatrixEvent,
4663
room: Room | undefined,

src/config.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,14 @@ const configSchema = z
6464
matrix: z
6565
.object({
6666
homeserver: z.string().url(),
67-
accessToken: z.string().min(1),
6867
defaultRoomId: z.string().min(1),
68+
accessToken: z.string().min(1).optional(),
69+
user: z.string().min(1).optional(),
70+
password: z.string().min(1).optional(),
71+
})
72+
.refine((m) => !!m.accessToken || (!!m.user && !!m.password), {
73+
message: 'Matrix requires MATRIX_ACCESS_TOKEN or both MATRIX_USER and MATRIX_PASSWORD',
74+
path: ['accessToken'],
6975
})
7076
.optional(),
7177
mattermost: z
@@ -161,13 +167,18 @@ function slackFromEnv(env: NodeJS.ProcessEnv) {
161167
}
162168

163169
function matrixFromEnv(env: NodeJS.ProcessEnv) {
164-
if (!optStr(env.MATRIX_HOMESERVER) && !optStr(env.MATRIX_ACCESS_TOKEN) && !optStr(env.MATRIX_DEFAULT_ROOM_ID)) {
165-
return undefined;
166-
}
170+
const homeserver = optStr(env.MATRIX_HOMESERVER);
171+
const accessToken = optStr(env.MATRIX_ACCESS_TOKEN);
172+
const user = optStr(env.MATRIX_USER);
173+
const password = optStr(env.MATRIX_PASSWORD);
174+
const defaultRoomId = optStr(env.MATRIX_DEFAULT_ROOM_ID);
175+
if (!homeserver && !accessToken && !user && !password && !defaultRoomId) return undefined;
167176
return {
168-
homeserver: optStr(env.MATRIX_HOMESERVER),
169-
accessToken: optStr(env.MATRIX_ACCESS_TOKEN),
170-
defaultRoomId: optStr(env.MATRIX_DEFAULT_ROOM_ID),
177+
homeserver,
178+
defaultRoomId,
179+
...(accessToken ? { accessToken } : {}),
180+
...(user ? { user } : {}),
181+
...(password ? { password } : {}),
171182
};
172183
}
173184

src/main.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,17 @@ if (config.output === 'console') {
110110
const slackNotifier =
111111
slackWeb && config.slack ? slackChat.createSlackChatNotifier({ slack: slackWeb }) : undefined;
112112

113-
const matrixNotifier: Notifier | undefined = config.matrix
113+
const matrixAuth = config.matrix?.accessToken
114+
? { accessToken: config.matrix.accessToken }
115+
: config.matrix?.user && config.matrix?.password
116+
? { user: config.matrix.user, password: config.matrix.password }
117+
: undefined;
118+
119+
const matrixNotifier: Notifier | undefined = config.matrix && matrixAuth
114120
? createPromiseBackedNotifier(
115121
startMatrixAdapter({
116122
homeserverUrl: config.matrix.homeserver,
117-
accessToken: config.matrix.accessToken,
123+
auth: matrixAuth,
118124
matrixSdkLevel: config.logging.matrixSdkLevel,
119125
defaultRoomId: config.matrix.defaultRoomId,
120126
onEvent,

test/config.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,5 +232,53 @@ describe('getConfig', () => {
232232
expect(cfg.slack?.botToken).toBe('xoxb-test');
233233
expect(cfg.matrix?.defaultRoomId).toBe('!room:example.com');
234234
});
235+
236+
it('accepts Matrix username + password as an alternative to access token', () => {
237+
const cfg = getConfig({
238+
MATRIX_HOMESERVER: 'https://matrix.example.com',
239+
MATRIX_USER: 'kaytoo',
240+
MATRIX_PASSWORD: 'pw',
241+
MATRIX_DEFAULT_ROOM_ID: '!room:example.com',
242+
OPENSEARCH_URL: 'https://opensearch.example.com',
243+
OPENSEARCH_USERNAME: 'user',
244+
OPENSEARCH_PASSWORD: 'pass',
245+
LLM_BASE_URL: 'https://llm.example.com',
246+
LLM_API_KEY: 'key',
247+
});
248+
249+
expect(cfg.output).toBe('chat');
250+
expect(cfg.matrix?.user).toBe('kaytoo');
251+
expect(cfg.matrix?.password).toBe('pw');
252+
expect(cfg.matrix?.accessToken).toBeUndefined();
253+
});
254+
255+
it('rejects Matrix config with neither access token nor user+password', () => {
256+
expect(() =>
257+
getConfig({
258+
MATRIX_HOMESERVER: 'https://matrix.example.com',
259+
MATRIX_DEFAULT_ROOM_ID: '!room:example.com',
260+
OPENSEARCH_URL: 'https://opensearch.example.com',
261+
OPENSEARCH_USERNAME: 'user',
262+
OPENSEARCH_PASSWORD: 'pass',
263+
LLM_BASE_URL: 'https://llm.example.com',
264+
LLM_API_KEY: 'key',
265+
}),
266+
).toThrowError(/MATRIX_ACCESS_TOKEN or both MATRIX_USER and MATRIX_PASSWORD/);
267+
});
268+
269+
it('rejects Matrix config with only a username and no password', () => {
270+
expect(() =>
271+
getConfig({
272+
MATRIX_HOMESERVER: 'https://matrix.example.com',
273+
MATRIX_USER: 'kaytoo',
274+
MATRIX_DEFAULT_ROOM_ID: '!room:example.com',
275+
OPENSEARCH_URL: 'https://opensearch.example.com',
276+
OPENSEARCH_USERNAME: 'user',
277+
OPENSEARCH_PASSWORD: 'pass',
278+
LLM_BASE_URL: 'https://llm.example.com',
279+
LLM_API_KEY: 'key',
280+
}),
281+
).toThrowError(/MATRIX_ACCESS_TOKEN or both MATRIX_USER and MATRIX_PASSWORD/);
282+
});
235283
});
236284

0 commit comments

Comments
 (0)