6 Commits

Author SHA1 Message Date
renovate[bot]
d548f7ba8c chore(deps): update docker.io/metio/matrix-alertmanager-receiver docker tag to v2026.2.4 2026-02-04 10:46:09 +02:00
Slavi Pantaleev
a7ddb189b5 Add missing license file for whoami_sync_worker_router.js.j2 2026-02-04 04:26:15 +02:00
Slavi Pantaleev
7d4536cf78 Upgrade baibot (v1.13.0 -> v1.14.0) and add built-in tools configuration support 2026-02-04 04:21:47 +02:00
Slavi Pantaleev
93f6264466 Add CHANGELOG entry for whoami-based sync worker routing 2026-02-04 04:06:59 +02:00
Slavi Pantaleev
45c855c853 Remove old map-based user identifier extraction for sync workers
The whoami-based approach is now the only implementation for sync worker routing.
It works with all token types (native Synapse, MAS, etc.) and is automatically
enabled when sync workers exist.

The old map-based approach only worked with native Synapse tokens (syt_<b64>_...)
and would give poor results with MAS or other auth systems.
2026-02-04 04:06:59 +02:00
Slavi Pantaleev
5cc69ca7eb Add whoami-based sync worker routing for user-level sticky sessions
This adds a new routing mechanism for sync workers that resolves access tokens
to usernames via Synapse's whoami endpoint, enabling true user-level sticky
routing regardless of which device or token is used.

Previously, sticky routing relied on parsing the username from native Synapse
tokens (`syt_<base64 username>_...`), which only works with native Synapse auth
and provides device-level stickiness at best. This new approach works with any
auth system (native Synapse, MAS, etc.) because Synapse handles token validation
internally.

Implementation uses nginx's auth_request module with an njs script because:
- The whoami lookup requires an async HTTP subrequest (ngx.fetch)
- js_set handlers must return synchronously and don't support async operations
- auth_request allows the async lookup to complete, then captures the result
  via response headers into nginx variables

The njs script:
- Extracts access tokens from Authorization header or query parameter
- Calls Synapse's whoami endpoint to resolve token -> username
- Caches results in a shared memory zone to minimize latency
- Returns the username via a `X-User-Identifier` header

The username is then used by nginx's upstream hash directive for consistent
worker selection. This leverages nginx's built-in health checking and failover.
2026-02-04 04:06:59 +02:00
12 changed files with 405 additions and 20 deletions

View File

@@ -1,3 +1,43 @@
# 2026-02-04
## baibot now supports OpenAI's built-in tools (Web Search and Code Interpreter)
**TLDR**: if you're using the [OpenAI provider](https://github.com/etkecc/baibot/blob/main/docs/providers.md#openai) with [baibot](docs/configuring-playbook-bot-baibot.md), you can now enable [built-in tools](https://github.com/etkecc/baibot/blob/61d18b2/docs/features.md#%EF%B8%8F-built-in-tools-openai-only) (`web_search` and `code_interpreter`) to extend the model's capabilities.
These tools are **disabled by default** and can be enabled via Ansible variables for static agent configurations:
```yaml
matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_tools_web_search: true
matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_tools_code_interpreter: true
```
Users who define agents dynamically at runtime will need to [update their agents](https://github.com/etkecc/baibot/blob/61d18b2/docs/agents.md#updating-agents) to enable these tools. See the [baibot v1.14.0 changelog](https://github.com/etkecc/baibot/blob/61d18b2/CHANGELOG.md) for details.
## Whoami-based sync worker routing for improved sticky sessions for Synapse
Deployments using [Synapse workers](./docs/configuring-playbook-synapse.md#load-balancing-with-workers) now benefit from improved sync worker routing via a new whoami-based mechanism (making use of the [whoami Matrix Client-Server API](https://spec.matrix.org/v1.17/client-server-api/#get_matrixclientv3accountwhoami)).
Previously, sticky routing for sync workers relied on parsing usernames from access tokens, which only worked with native Synapse tokens (`syt_<base64 username>_...`). This approach failed for [Matrix Authentication Service](docs/configuring-playbook-matrix-authentication-service.md) (MAS) deployments, where tokens are opaque and don't contain username information. This resulted in device-level stickiness (same token → same worker) rather than user-level stickiness (same user → same worker regardless of device), leading to suboptimal cache utilization on sync workers.
The new implementation calls Synapse's `/whoami` endpoint to resolve access tokens to usernames, enabling proper user-level sticky routing regardless of the authentication system in use (native Synapse auth, MAS, etc.). Results are cached to minimize overhead.
This change:
- **Automatically enables** when sync workers are configured (no action required)
- **Works universally** with any authentication system
- **Replaces the old implementation** entirely to keep the codebase simple
- **Adds minimal overhead** (one cached internal subrequest per sync request) for non-MAS deployments
For debugging, you can enable verbose logging and/or response headers showing routing decisions:
```yaml
# Logs cache hits/misses and routing decisions to the container's stderr
matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_logging_enabled: true
# Adds X-Sync-Worker-Router-User-Identifier and X-Sync-Worker-Router-Upstream headers to sync responses
matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_debug_headers_enabled: true
```
# 2025-12-09
## Traefik Cert Dumper upgrade

View File

@@ -243,6 +243,12 @@ matrix_bot_baibot_config_agents_static_definitions_openai_config_api_key: "YOUR_
# If you'd like to use another text-generation agent, uncomment and adjust:
# matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_model_id: gpt-4.1
# Uncomment below to enable OpenAI's built-in tools.
# These tools are disabled by default. Enabling them may incur additional costs.
# See: https://github.com/etkecc/baibot/blob/61d18b2/docs/features.md#%EF%B8%8F-built-in-tools-openai-only
# matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_tools_web_search: true
# matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_tools_code_interpreter: true
```
Because this is a [statically](https://github.com/etkecc/baibot/blob/main/docs/configuration/README.md#static-configuration)-defined agent, it will be given a `static/` ID prefix and will be named `static/openai`.

View File

@@ -11,7 +11,7 @@
matrix_alertmanager_receiver_enabled: true
# renovate: datasource=docker depName=docker.io/metio/matrix-alertmanager-receiver
matrix_alertmanager_receiver_version: 2026.1.31
matrix_alertmanager_receiver_version: 2026.2.4
matrix_alertmanager_receiver_scheme: https

View File

@@ -17,7 +17,7 @@ matrix_bot_baibot_container_repo_version: "{{ 'main' if matrix_bot_baibot_versio
matrix_bot_baibot_container_src_files_path: "{{ matrix_base_data_path }}/baibot/container-src"
# renovate: datasource=docker depName=ghcr.io/etkecc/baibot
matrix_bot_baibot_version: v1.13.0
matrix_bot_baibot_version: v1.14.0
matrix_bot_baibot_container_image: "{{ matrix_bot_baibot_container_image_registry_prefix }}etkecc/baibot:{{ matrix_bot_baibot_version }}"
matrix_bot_baibot_container_image_registry_prefix: "{{ 'localhost/' if matrix_bot_baibot_container_image_self_build else matrix_bot_baibot_container_image_registry_prefix_upstream }}"
matrix_bot_baibot_container_image_registry_prefix_upstream: "{{ matrix_bot_baibot_container_image_registry_prefix_upstream_default }}"
@@ -395,6 +395,11 @@ matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation
matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_max_response_tokens: ~
matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_max_completion_tokens: 128000
matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_max_context_tokens: 400000
# Built-in tools configuration (OpenAI only).
# These tools extend the model's capabilities but are disabled by default following upstream defaults.
# See: https://github.com/etkecc/baibot/blob/main/docs/features.md#%EF%B8%8F-built-in-tools-openai-only
matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_tools_web_search: false
matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_tools_code_interpreter: false
matrix_bot_baibot_config_agents_static_definitions_openai_config_speech_to_text_enabled: true
matrix_bot_baibot_config_agents_static_definitions_openai_config_speech_to_text_model_id: whisper-1

View File

@@ -15,6 +15,9 @@ text_generation:
max_completion_tokens: {{ matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_max_completion_tokens | int | to_json }}
{% endif %}
max_context_tokens: {{ matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_max_context_tokens | int | to_json }}
tools:
web_search: {{ matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_tools_web_search | to_json }}
code_interpreter: {{ matrix_bot_baibot_config_agents_static_definitions_openai_config_text_generation_tools_code_interpreter | to_json }}
{% endif %}
{% if matrix_bot_baibot_config_agents_static_definitions_openai_config_speech_to_text_enabled %}

View File

@@ -28,6 +28,7 @@ matrix_synapse_reverse_proxy_companion_version: 1.29.4-alpine
matrix_synapse_reverse_proxy_companion_base_path: "{{ matrix_synapse_base_path }}/reverse-proxy-companion"
matrix_synapse_reverse_proxy_companion_confd_path: "{{ matrix_synapse_reverse_proxy_companion_base_path }}/conf.d"
matrix_synapse_reverse_proxy_companion_njs_path: "{{ matrix_synapse_reverse_proxy_companion_base_path }}/njs"
# List of systemd services that matrix-synapse-reverse-proxy-companion.service depends on
matrix_synapse_reverse_proxy_companion_systemd_required_services_list: "{{ matrix_synapse_reverse_proxy_companion_systemd_required_services_list_default + matrix_synapse_reverse_proxy_companion_systemd_required_services_list_auto + matrix_synapse_reverse_proxy_companion_systemd_required_services_list_custom }}"
@@ -290,3 +291,73 @@ matrix_synapse_reverse_proxy_companion_synapse_cache_proxy_cache_valid_time: "24
# As such, it trusts the protocol scheme forwarded by the upstream proxy.
matrix_synapse_reverse_proxy_companion_trust_forwarded_proto: true
matrix_synapse_reverse_proxy_companion_x_forwarded_proto_value: "{{ '$http_x_forwarded_proto' if matrix_synapse_reverse_proxy_companion_trust_forwarded_proto else '$scheme' }}"
########################################################################################
# #
# njs module #
# #
########################################################################################
# Controls whether the njs module is loaded.
matrix_synapse_reverse_proxy_companion_njs_enabled: "{{ matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_enabled }}"
########################################################################################
# #
# /njs module #
# #
########################################################################################
########################################################################################
# #
# Whoami-based sync worker routing #
# #
########################################################################################
# Controls whether the whoami-based sync worker router is enabled.
# When enabled, the reverse proxy will call Synapse's /_matrix/client/v3/account/whoami endpoint
# to resolve access tokens to usernames, allowing consistent routing of requests from the same user
# to the same sync worker regardless of which device or token they use.
#
# This works with any authentication system (native Synapse auth, MAS, etc.) because Synapse
# handles the token validation internally.
#
# Enabled by default when there are sync workers, because sync workers benefit from user-level
# stickiness due to their per-user in-memory caches.
matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_enabled: "{{ matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'sync_worker') | list | length > 0 }}"
# The whoami endpoint path (Matrix spec endpoint).
matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_endpoint: /_matrix/client/v3/account/whoami
# The full URL to the whoami endpoint.
matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_url: "http://{{ matrix_synapse_reverse_proxy_companion_client_api_addr }}{{ matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_endpoint }}"
# Cache duration (in seconds) for whoami lookup results.
# Token -> username mappings are cached to avoid repeated whoami calls.
# A longer TTL reduces load on Synapse but means username changes take longer to take effect.
matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_cache_ttl_seconds: 3600
# Size of the shared memory zone for caching whoami results (in megabytes).
# Each cached entry is approximately 100-200 bytes.
matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_cache_size_mb: 1
# Controls whether verbose logging is enabled for the whoami sync worker router.
# When enabled, logs cache hits/misses and routing decisions.
# Useful for debugging, but should be disabled in production.
matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_logging_enabled: false
# The length of the access token to show in logs when logging is enabled.
# Keeping this short is a good idea from a security perspective.
matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_logging_token_length: 12
# Controls whether debug response headers are added to sync requests.
# When enabled, adds X-Sync-Worker-Router-User-Identifier and X-Sync-Worker-Router-Upstream headers.
# Useful for debugging routing behavior, but should be disabled in production.
matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_debug_headers_enabled: false
########################################################################################
# #
# /Whoami-based sync worker routing #
# #
########################################################################################

View File

@@ -7,14 +7,16 @@
- name: Ensure matrix-synapse-reverse-proxy-companion paths exist
ansible.builtin.file:
path: "{{ item }}"
path: "{{ item.path }}"
state: directory
mode: 0750
owner: "{{ matrix_user_name }}"
group: "{{ matrix_group_name }}"
with_items:
- "{{ matrix_synapse_reverse_proxy_companion_base_path }}"
- "{{ matrix_synapse_reverse_proxy_companion_confd_path }}"
- {path: "{{ matrix_synapse_reverse_proxy_companion_base_path }}", when: true}
- {path: "{{ matrix_synapse_reverse_proxy_companion_confd_path }}", when: true}
- {path: "{{ matrix_synapse_reverse_proxy_companion_njs_path }}", when: "{{ matrix_synapse_reverse_proxy_companion_njs_enabled }}"}
when: item.when | bool
- name: Ensure matrix-synapse-reverse-proxy-companion is configured
ansible.builtin.template:
@@ -33,6 +35,21 @@
- src: "{{ role_path }}/templates/labels.j2"
dest: "{{ matrix_synapse_reverse_proxy_companion_base_path }}/labels"
- name: Ensure matrix-synapse-reverse-proxy-companion whoami sync worker router njs script is deployed
ansible.builtin.template:
src: "{{ role_path }}/templates/nginx/njs/whoami_sync_worker_router.js.j2"
dest: "{{ matrix_synapse_reverse_proxy_companion_njs_path }}/whoami_sync_worker_router.js"
owner: "{{ matrix_user_name }}"
group: "{{ matrix_group_name }}"
mode: 0644
when: matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_enabled | bool
- name: Ensure matrix-synapse-reverse-proxy-companion njs path is removed when njs is disabled
ansible.builtin.file:
path: "{{ matrix_synapse_reverse_proxy_companion_njs_path }}"
state: absent
when: not matrix_synapse_reverse_proxy_companion_njs_enabled
- name: Ensure matrix-synapse-reverse-proxy-companion nginx container image is pulled
community.docker.docker_image:
name: "{{ matrix_synapse_reverse_proxy_companion_container_image }}"

View File

@@ -41,20 +41,29 @@
{% endfor %}
{% endmacro %}
{% macro render_locations_to_upstream_with_whoami_sync_worker_router(locations, upstream_name) %}
{% for location in locations %}
location ~ {{ location }} {
# Use auth_request to call the whoami sync worker router.
# The handler resolves the access token to a user identifier and returns it
# in the X-User-Identifier header, which is then used for upstream hashing.
auth_request /_whoami_sync_worker_router;
auth_request_set $user_identifier $sent_http_x_user_identifier;
{% if matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_debug_headers_enabled %}
add_header X-Sync-Worker-Router-User-Identifier $user_identifier always;
add_header X-Sync-Worker-Router-Upstream $upstream_addr always;
{% endif %}
proxy_pass http://{{ upstream_name }}$request_uri;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
{% endfor %}
{% endmacro %}
{% if matrix_synapse_reverse_proxy_companion_synapse_workers_enabled %}
# Maps from https://tcpipuk.github.io/synapse/deployment/nginx.html#mapsconf
# Client username from access token
map $arg_access_token $accesstoken_from_urlparam {
default $arg_access_token;
"~syt_(?<username>.*?)_.*" $username;
}
# Client username from MXID
map $http_authorization $mxid_localpart {
default $http_authorization;
"~Bearer syt_(?<username>.*?)_.*" $username;
"" $accesstoken_from_urlparam;
}
# Whether to upgrade HTTP connection
map $http_upgrade $connection_upgrade {
default upgrade;
@@ -76,7 +85,7 @@ map $request_uri $room_name {
{% endif %}
{% if sync_workers | length > 0 %}
{{- render_worker_upstream('sync_workers_upstream', sync_workers, 'hash $mxid_localpart consistent;') }}
{{- render_worker_upstream('sync_workers_upstream', sync_workers, 'hash $user_identifier consistent;') }}
{% endif %}
{% if client_reader_workers | length > 0 %}
@@ -134,6 +143,17 @@ server {
proxy_max_temp_file_size 0;
proxy_set_header Host $host;
{% if matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_enabled %}
# Internal location for whoami-based sync worker routing.
# This is called via auth_request from sync worker locations.
# The njs handler calls the whoami endpoint to resolve access tokens to usernames,
# then returns the username in the X-User-Identifier header for upstream hashing.
location = /_whoami_sync_worker_router {
internal;
js_content whoami_sync_worker_router.handleAuthRequest;
}
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_synapse_workers_enabled %}
# Client-server overrides — These locations must go to the main Synapse process
location ~ {{ matrix_synapse_reverse_proxy_companion_client_server_main_override_locations_regex }} {
@@ -207,7 +227,7 @@ server {
# sync workers
# https://tcpipuk.github.io/synapse/deployment/workers.html
# https://tcpipuk.github.io/synapse/deployment/nginx.html#locationsconf
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_sync_worker_client_server_locations, 'sync_workers_upstream') }}
{{ render_locations_to_upstream_with_whoami_sync_worker_router(matrix_synapse_reverse_proxy_companion_synapse_sync_worker_client_server_locations, 'sync_workers_upstream') }}
{% endif %}
{% if client_reader_workers | length > 0 %}

View File

@@ -8,6 +8,12 @@
# - various temp paths are changed to `/tmp`, so that a non-root user can write to them
# - the `user` directive was removed, as we don't want nginx to switch users
# load_module directives must be first or nginx will choke with:
# > [emerg] "load_module" directive is specified too late.
{% if matrix_synapse_reverse_proxy_companion_njs_enabled %}
load_module modules/ngx_http_js_module.so;
{% endif %}
worker_processes {{ matrix_synapse_reverse_proxy_companion_worker_processes }};
error_log /var/log/nginx/error.log warn;
pid /tmp/nginx.pid;
@@ -22,7 +28,6 @@ events {
{% endfor %}
}
http {
proxy_temp_path /tmp/proxy_temp;
client_body_temp_path /tmp/client_temp;
@@ -33,6 +38,16 @@ http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
{% if matrix_synapse_reverse_proxy_companion_njs_enabled %}
js_path /njs/;
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_enabled %}
# njs module for whoami-based sync worker routing
js_import whoami_sync_worker_router from whoami_sync_worker_router.js;
js_shared_dict_zone zone=whoami_sync_worker_router_cache:{{ matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_cache_size_mb }}m;
{% endif %}
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

View File

@@ -0,0 +1,202 @@
#jinja2: lstrip_blocks: True
// Whoami-based sync worker router
//
// This script resolves access tokens to usernames by calling the whoami endpoint.
// Results are cached to minimize latency impact. The username is returned via the
// X-User-Identifier header, which nginx captures and uses for upstream hashing.
//
// This works with any authentication system (native Synapse auth, MAS, etc.) because
// Synapse handles token validation internally.
//
// Why auth_request instead of js_set?
// -----------------------------------
// A simpler approach would be to use js_set to populate a variable (e.g., $user_identifier)
// and then use that variable in an upstream's `hash` directive. However, this doesn't work
// because:
//
// 1. The whoami lookup requires an HTTP subrequest (ngx.fetch), which is asynchronous.
// 2. js_set handlers must return synchronously - nginx's variable evaluation doesn't support
// async operations. Using async functions with js_set causes errors like:
// "async operation inside variable handler"
//
// The auth_request approach solves this by:
// 1. Making a subrequest to an internal location that uses js_content (which supports async)
// 2. Returning the user identifier via a response header (X-User-Identifier)
// 3. Capturing that header with auth_request_set into $user_identifier
// 4. Using $user_identifier in the upstream's hash directive for consistent routing
const WHOAMI_URL = {{ matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_url | to_json }};
const CACHE_TTL_MS = {{ (matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_cache_ttl_seconds * 1000) | to_json }};
const LOGGING_ENABLED = {{ matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_logging_enabled | to_json }};
const LOGGING_TOKEN_LENGTH = {{ matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_logging_token_length | to_json }};
function log(message) {
if (LOGGING_ENABLED) {
// Using WARN level because nginx's error_log is hardcoded to 'warn' and our logs won't be visible otherwise
ngx.log(ngx.WARN, 'whoami_sync_worker_router: ' + message);
}
}
// Truncate token for logging (show first X chars only for security)
function truncateToken(token) {
if (!token || token.length <= LOGGING_TOKEN_LENGTH) {
return token;
}
return token.substring(0, LOGGING_TOKEN_LENGTH) + '...';
}
// Extract token from request (Authorization header or query parameter)
function extractToken(r) {
// Try Authorization header first
const authHeader = r.headersIn['Authorization'];
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7);
}
// Fall back to access_token query parameter (deprecated in Matrix v1.11, but homeservers must support it)
if (r.args.access_token) {
return r.args.access_token;
}
return null;
}
// Extract localpart from user_id (e.g., "@alice:example.com" -> "alice")
function extractLocalpart(userId) {
if (!userId || !userId.startsWith('@')) {
return null;
}
const colonIndex = userId.indexOf(':');
if (colonIndex === -1) {
return null;
}
return userId.substring(1, colonIndex);
}
// Get cached username for token
function getCachedUsername(token) {
const cache = ngx.shared.whoami_sync_worker_router_cache;
if (!cache) {
return null;
}
const entry = cache.get(token);
if (entry) {
try {
const data = JSON.parse(entry);
if (data.expires > Date.now()) {
log('cache hit for token ' + truncateToken(token) + ' -> ' + data.username);
return data.username;
}
// Expired, remove from cache
log('cache expired for token ' + truncateToken(token));
cache.delete(token);
} catch (e) {
cache.delete(token);
}
}
return null;
}
// Cache username for token
function cacheUsername(token, username) {
const cache = ngx.shared.whoami_sync_worker_router_cache;
if (!cache) {
return;
}
try {
const entry = JSON.stringify({
username: username,
expires: Date.now() + CACHE_TTL_MS
});
cache.set(token, entry);
log('cached token ' + truncateToken(token) + ' -> ' + username);
} catch (e) {
// Cache full or other error, log and continue
ngx.log(ngx.WARN, 'whoami_sync_worker_router: cache error: ' + e.message);
}
}
// Call whoami endpoint to get user_id
async function lookupWhoami(token) {
log('performing whoami lookup for token ' + truncateToken(token));
try {
const response = await ngx.fetch(WHOAMI_URL, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token
}
});
if (response.ok) {
const data = await response.json();
if (data.user_id) {
const localpart = extractLocalpart(data.user_id);
log('whoami lookup success: ' + data.user_id + ' -> ' + localpart);
return localpart;
}
} else if (response.status === 401) {
// Token is invalid/expired - this is expected for some requests
log('whoami lookup returned 401 (invalid/expired token)');
return null;
} else {
ngx.log(ngx.WARN, 'whoami_sync_worker_router: whoami returned status ' + response.status);
}
} catch (e) {
ngx.log(ngx.ERR, 'whoami_sync_worker_router: whoami failed: ' + e.message);
}
return null;
}
// Set response header with the user identifier for upstream hashing
function setUserIdentifier(r, identifier) {
log('resolved user identifier: ' + identifier);
r.headersOut['X-User-Identifier'] = identifier;
}
// Main handler for auth_request subrequest.
// Returns 200 with X-User-Identifier header containing the user identifier for upstream hashing.
async function handleAuthRequest(r) {
const token = extractToken(r);
if (!token) {
// No token found (e.g., OPTIONS preflight requests don't include Authorization header).
// We return a random value to distribute these requests across workers.
// Returning an empty string would cause all no-token requests to hash to the same value,
// routing them all to a single worker.
// This doesn't affect the cache since we only cache token -> username mappings.
log('no token found in request, distributing randomly');
setUserIdentifier(r, '_no_token_' + Math.random());
r.return(200);
return;
}
// Check cache first
const cachedUsername = getCachedUsername(token);
if (cachedUsername) {
setUserIdentifier(r, cachedUsername);
r.return(200);
return;
}
// Perform whoami lookup
log('cache miss for token ' + truncateToken(token));
const username = await lookupWhoami(token);
if (username) {
cacheUsername(token, username);
setUserIdentifier(r, username);
r.return(200);
return;
}
// Whoami lookup failed, fall back to using the token itself for hashing.
// This still provides device-level sticky routing (same token -> same worker).
log('whoami lookup failed, falling back to token-based routing');
setUserIdentifier(r, token);
r.return(200);
}
export default { handleAuthRequest };

View File

@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2026 Slavi Pantaleev
SPDX-License-Identifier: AGPL-3.0-or-later

View File

@@ -36,6 +36,9 @@ ExecStartPre={{ devture_systemd_docker_base_host_command_docker }} create \
{% endif %}
--mount type=bind,src={{ matrix_synapse_reverse_proxy_companion_base_path }}/nginx.conf,dst=/etc/nginx/nginx.conf,ro \
--mount type=bind,src={{ matrix_synapse_reverse_proxy_companion_confd_path }},dst=/etc/nginx/conf.d,ro \
{% if matrix_synapse_reverse_proxy_companion_njs_enabled %}
--mount type=bind,src={{ matrix_synapse_reverse_proxy_companion_njs_path }},dst=/njs,ro \
{% endif %}
--label-file={{ matrix_synapse_reverse_proxy_companion_base_path }}/labels \
{% for arg in matrix_synapse_reverse_proxy_companion_container_arguments %}
{{ arg }} \