Skip to content

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

124 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Alkemio Matrix Adapter (Go)

A high-performance, stateless Matrix Adapter service written in Go, implementing the Alkemio Matrix Adapter specification.

Build Status Coverage Status Deploy to DockerHub

This repository contains two core elements:

  • Matrix Adapter Service: The Go application that provides the runtime service.
  • Matrix Adapter Library: A TypeScript library (lib/) providing shared payloads and event types for clients of the service.

Features

  • Hexagonal Architecture: Clean separation of core domain, ports, and adapters.
  • Stateless Design: Designed for horizontal scalability.
  • Matrix AppService: Connects to Matrix Homeserver as an Application Service.
  • RabbitMQ Integration: Consumes commands and publishes events via AMQP.
  • Type-Safe DTOs: Shared contract definitions with TypeScript generation.
  • Actor ID Support: Native support for Alkemio Actor IDs (UUIDs) mapped to Matrix IDs.

Prerequisites

  • Go 1.25+
  • Docker & Docker Compose
  • Matrix Homeserver (Synapse/Dendrite)
  • RabbitMQ
  • Node.js 22+ / pnpm 9+ (for Library)

Configuration

The service is configured via environment variables or a config.yaml file. The environment variables are compatible with the legacy TypeScript service.

Variable Description Default
ENVIRONMENT Environment (development, production) development
LOGGING_LEVEL_CONSOLE Logging level (debug, info, warn, error) info
SYNAPSE_SERVER_URL URL of the Matrix Homeserver -
SYNAPSE_HOMESERVER_NAME Matrix Homeserver Name -
MATRIX_AS_TOKEN AppService Token (as_token) -
MATRIX_HS_TOKEN Homeserver Token (hs_token) -
MATRIX_BOT_ACTOR_ID Bot Actor ID (UUID), also used as Matrix localpart 00000000-0000-0000-0000-000000000000
MATRIX_BOT_DISPLAY_NAME Display name for the bot user in Matrix Alkemio
SYNAPSE_SERVER_SHARED_SECRET Synapse registration_shared_secret for auto-promoting bot to server admin -
FILE_SERVICE_MAX_ATTACHMENT_BYTES Per-attachment byte cap for outbound media. Must parse as an integer — a malformed value fails the boot. A non-positive value falls back to the default. Transfers must also complete within the operation budget described under Media Attachments 52428800 (50 MiB)
RABBITMQ_URL Full AMQP Connection URL -
RABBITMQ_HOST RabbitMQ Host (if URL not set) -
RABBITMQ_PORT RabbitMQ Port (if URL not set) 5672
RABBITMQ_USER RabbitMQ User (if URL not set) -
RABBITMQ_PASSWORD RabbitMQ Password (if URL not set) -

Running Locally

  1. Install Dependencies:

    make deps
  2. Run the Service:

    make run

Building

Service (Go)

make build

Library (TypeScript)

cd lib
pnpm install
pnpm build

Docker

Build Image

make docker-build

Run with Docker Compose

docker-compose up --build

Development

  • Linting: make lint
  • Testing: make test
  • Test Coverage: make test-coverage
  • Format: make fmt
  • Generate DTOs: make generate (Updates Go DTOs and generates TypeScript definitions in lib/)

TypeScript Library Publishing

The shared TypeScript library (lib/) is automatically published via GitHub Actions workflows.

Publishing Targets

Trigger Registry Package Name Version Tag
Git tag (v*) npmjs @alkemio/matrix-adapter-lib From tag (e.g., v1.2.3 → 1.2.3) latest
Pull Request pkg.pr.new @alkemio/matrix-adapter-lib Commit-based preview URL —
Manual dispatch pkg.pr.new @alkemio/matrix-adapter-lib Commit-based preview URL —

Release Process

  1. Development: PRs that modify lib/ or pkg/dto/ automatically publish preview versions via pkg.pr.new for testing.

  2. Production Release: Create a git tag to publish to npmjs:

    git tag v1.2.3
    git push origin v1.2.3
  3. Install from npmjs (production):

    npm install @alkemio/matrix-adapter-lib
  4. Install preview (development/testing):

    npm install https://pkg.pr.new/alkem-io/matrix-adapter/@alkemio/matrix-adapter-lib@{commit-sha}

Authentication

  • npmjs: Uses OIDC trusted publishing (no token required) with provenance attestation.
  • pkg.pr.new: No authentication required — uses the pkg.pr.new GitHub App.

Architecture

The project follows a Hexagonal (Ports & Adapters) architecture:

  • cmd/: Entrypoints (main application wiring).
  • internal/app/: Application lifecycle, dependency injection, and startup logic.
  • internal/config/: Configuration loading and validation.
  • internal/core/domain: Pure business logic and entities.
  • internal/core/ports: Interfaces defining interactions with the outside world.
  • internal/core/service: Implementation of business use cases.
  • internal/infrastructure: Adapters for external systems (Matrix, RabbitMQ, HTTP).
  • pkg/dto: Data Transfer Objects shared with external consumers (source for tygo generation).
  • lib/: Shared TypeScript library containing generated DTOs and event types.

Actor ID Usage

The Matrix Adapter uses Actor IDs (UUIDs) to identify users in external APIs.

  • Actor ID: The Alkemio agent ID (agent.id) - used in all RabbitMQ commands and events
  • Matrix ID: The Matrix user identifier (e.g., @uuid:server)

Actor IDs are used directly as Matrix localparts (@actor-uuid:server).

All external APIs (RabbitMQ commands and events) always use Actor IDs.

Protocol

The Matrix Adapter implements a structured RabbitMQ protocol for communication with the Alkemio Server. See MatrixAdapterProtocol_V3.md for the full specification.

Supported Commands

Category Command Topic
Room Create Room communication.room.create
Get Room communication.room.get
Update Room communication.room.update
Delete Room communication.room.delete
List Rooms communication.room.list
Get Room Members communication.room.members.get
Batch Add Member communication.room.member.batch.add
Batch Remove Member communication.room.member.batch.remove
Space Create Space communication.space.create
Get Space communication.space.get
Update Space communication.space.update
Delete Space communication.space.delete
List Spaces communication.space.list
Batch Add Member communication.space.member.batch.add
Batch Remove Member communication.space.member.batch.remove
Hierarchy Set Parent communication.hierarchy.set_parent
Message Send Message communication.message.send
Get Message communication.message.get
Delete Message communication.message.delete
Reaction Add Reaction communication.reaction.add
Remove Reaction communication.reaction.remove
Get Reaction communication.reaction.get
Thread Get Thread Messages communication.thread.messages.get
Actor Sync Actor Profile communication.actor.sync
Read Receipts Mark Message Read communication.message.read
Get Unread Counts communication.room.unread_counts.get
Custom State Set Room State communication.room.state.set
Get Room State communication.room.state.get
Set Space State communication.space.state.set
Get Space State communication.space.state.get

Outgoing Events

Event Topic Description
Message Received communication.message.received Emitted when a message is received in a room
DM Requested communication.room.dm.requested Emitted when a DM room creation is requested via webhook
Reaction Added communication.reaction.added Emitted when a user adds a reaction to a message
Reaction Removed communication.reaction.removed Emitted when a user removes a reaction from a message
Room Member Left communication.room.member.left Emitted when a user leaves or is kicked from a room
Read Receipt Updated communication.room.receipt.updated Emitted when a user's read position is updated
Message Edited communication.message.edited Emitted when a message is edited
Message Redacted communication.message.redacted Emitted when a message is deleted/redacted
Room Created communication.room.created Emitted when a room is created in Matrix
Room Member Updated communication.room.member.updated Emitted when a user's membership status changes (join, invite, etc.)
Room Updated communication.room.updated Emitted when a room's name, avatar, or topic changes
Space Updated communication.space.updated Emitted when a space's name, avatar, or topic changes

Media Attachments

communication.message.send publishes non-empty content, one existing_media reference, or both. Media sends contain a local media_id, display name and actual MIME/size/dimensions; they publish as the sender without transferring bytes again or emitting a document-ID hint. When content accompanies the media it becomes the Matrix media caption: body carries the caption and filename the display name. Without a caption, body is the display name and filename is omitted.

Browsers POST raw original bytes to /api/private/rest/messaging/media/upload?filename=.... The gateway strips the /api/private prefix and client identity headers, resolves the authenticated actor, and forwards to /rest/messaging/media/upload. The adapter accepts only the trusted X-Alkemio-Actor-Id UUID; absent, anonymous and guest identities are rejected. Uploads are room-neutral. Sending and placement apply current destination checks.

Every upload needs an exact Content-Length within the existing attachment-size cap. A body that is not an image streams directly to Synapse; the adapter neither accumulates nor spools it. A JPEG, PNG or WebP body, recognised from its bytes rather than its declared type, is read whole into memory and stored without its location, device and time metadata, with the content type and length of the rewritten bytes; of its Exif data only the orientation tag is kept. A corrupt image is refused with 400 invalid_media. Images share a fixed memory budget, and an image that cannot get its share within a short wait is refused with 503 upload_busy and a Retry-After header. An image body that stalls is cut off with 408 upload_timeout. Zero bytes use the SDK empty-body form. Failed uploads and uncertain sends are not replayed automatically. timeout_ms bounds message publication below the caller RPC wait; it defaults to 25 seconds.

Incoming events and history expose local media references plus event metadata. Old io.alkemio.document_id hints are ignored. The server handles shared bucket placement; the adapter does not move or delete stored files. Redacting an event does not delete shared media bytes.

Attachment normalization inventory

The read-only operator command exports retained local-media events for an explicit Alkemio room inventory. Run the built adapter with its normal Matrix connection configuration and existing administrator appservice credentials:

matrix-adapter attachments-export --rooms room-list.json --output events.jsonl

room-list.json is an object with schemaVersion: 1, scope set to fixture or supported-room-inventory, and roomIds containing Alkemio room UUIDs. The command resolves their canonical aliases, verifies the observed alias mapping, and scans all history pages including thread replies. It does not start the service, join rooms, register users, reconcile rooms, or access a database. A room whose alias Synapse reports as not found was never created in Matrix: it is recorded as room_absent with no history and counts as covered. Other alias failures and history failures are recorded and make the command exit unsuccessfully.

The command exclusively creates events.jsonl and events.jsonl.coverage.json with mode 0600; existing output files are never overwritten. Events contain only room/event/media identifiers, actor ID, sanitized filename, optional thread root, and timestamp. Document hints are not exported. Tokens, message text and content hashes are not exported. The sidecar records requested/resolved rooms, mapping and pagination completion, counts, and safe failure or skipped-event codes. Redacted, encrypted or unmappable retained events remain explicit limitations even when pagination completes.

complete: true certifies the requested scan, not a complete production inventory or permission to remove staging files. The server normalization tool must compare the exact current supported-room inventory and reject unsafe skipped markers. Keep a controlled operator window without concurrent historical changes between inventory and apply. Production normalization is a separate operator action; running this exporter never changes attachment storage.

DM Room Creation Flow

The adapter supports controlled DM (Direct Message) room creation through a webhook-based flow. This allows Synapse to request approval from the Alkemio Server before creating DM rooms.

Flow Overview

┌─────────┐        ┌──────────────┐        ┌─────────────────┐        ┌───────────┐
│ Synapse │  1.    │   Adapter    │   2.   │  Alkemio Server │   3.   │  Adapter  │
│  Spam   │───────▶│   Webhook    │───────▶│   (via RabbitMQ)│───────▶│  (via     │
│ Checker │ POST   │   Handler    │ Publish│                 │ Command│  RabbitMQ)│
└─────────┘        └──────────────┘        └─────────────────┘        └───────────┘
                                                                            │
                                                    4. Create DM Room       │
                                                    (type: "direct")        ▼
                                                                     ┌───────────┐
                                                                     │   Matrix  │
                                                                     │Homeserver │
                                                                     └───────────┘
  1. Synapse Spam Checker calls the adapter's webhook when a user attempts to create a DM
  2. Adapter publishes DMRequestedEvent to communication.room.dm.requested topic
  3. Alkemio Server processes the request and sends communication.room.create with type: "direct"
  4. Adapter creates the DM room in Matrix using existing room creation logic

Webhook Endpoint

Method Path Auth
POST /_matrix/app/alkemio/dm-request Bearer token (HS token)

Request Payload

{
  "inviter": "@550e8400-e29b-41d4-a716-446655440001:matrix.alkemio.org",
  "invitee": "@660e8400-e29b-41d4-a716-446655440002:matrix.alkemio.org"
}

Response

Success (202 Accepted):

{"status": "accepted"}

Error responses use standard HTTP error codes with JSON error messages:

  • 401 Unauthorized: Invalid or missing Bearer token
  • 400 Bad Request: Invalid payload or missing fields
  • 500 Internal Server Error: Failed to publish event

DM Room Creation Command

The Alkemio Server creates DM rooms using the existing communication.room.create command with type: "direct":

{
  "alkemio_room_id": "new-uuid-for-dm-room",
  "type": "direct",
  "name": "DM: User A - User B",
  "initial_members": ["actor-uuid-1", "actor-uuid-2"],
  "join_rule": "invite"
}

Response Structure

All responses follow a standard envelope:

{
  "success": true,
  "error": null
}

Error responses include structured error information:

{
  "success": false,
  "error": {
    "code": "ROOM_NOT_FOUND",
    "message": "Room with ID xyz does not exist",
    "details": "Optional technical details"
  }
}

Error Codes

Code Description
INVALID_PARAM Request validation failed (invalid payload or parameters)
ROOM_NOT_FOUND Referenced room does not exist
SPACE_NOT_FOUND Referenced space does not exist
ACTOR_NOT_FOUND Referenced actor does not exist
MATRIX_ERROR Matrix SDK/homeserver error
INTERNAL_ERROR Unexpected system error
NOT_ALLOWED Operation not permitted

Bot Architecture

The adapter's bot user (@00000000-...:domain) operates as a Synapse server admin but is not a member of rooms (only spaces). This architecture minimizes the bot's footprint in user-visible room lists.

Bot Behavior

  • Spaces: Bot stays as a member (required for hierarchy operations)
  • Rooms: Bot leaves after creation; re-joins temporarily only if no ghost users are available for writes
  • Reads: All room reads (state, messages, members) use the Synapse Admin API — no membership required
  • Writes: State events are sent via ghost users (the member with the highest power level)
  • Invites: Ghost users are joined directly via EnsureJoined — no invite events sent to clients

Server Admin Bootstrap

On startup, the adapter ensures the bot is a Synapse server admin:

  1. Already admin → proceeds normally
  2. SYNAPSE_SERVER_SHARED_SECRET set → registers bot as admin via shared secret, or creates a temporary admin to promote the bot
  3. No secret → logs a warning with SQL instructions

Custom State Events (io.alkemio.*)

The adapter supports setting/reading custom state events with the io.alkemio. prefix on any room or space. These are used for:

  • io.alkemio.visibility: Controls whether a room appears in Element via the Synapse module
  • Custom metadata for rooms/spaces managed by the Alkemio platform

Custom state can be set at room creation (via custom_state field) or independently via the state.set/get API endpoints.

Synapse Configuration

The following Synapse homeserver.yaml settings are required:

# Allow bot to publish rooms to the public directory
room_list_publication_rules:
  - user_id: "@00000000-0000-0000-0000-000000000000:your.domain"
    action: allow
  - action: deny

# Optional: Enable admin bootstrap (alternative to manual DB setup)
registration_shared_secret: "your-secret"

Synapse Module

The adapter includes a companion Synapse module (alkemio_room_control.py) that:

  1. Controls room creation — only the AppService bot can create rooms
  2. Filters /sync responses — hides rooms with io.alkemio.visibility: {visible: false} from Element
  3. DM webhook — notifies the adapter when users attempt to create DMs from Element

Synapse AppService Registration

The adapter requires an AppService registration file on Synapse. The bot uses MATRIX_BOT_ACTOR_ID as its Matrix localpart, following the same UUID pattern as all other actors.

Example Registration (registration.yaml)

id: alkemio-matrix-adapter
url: "http://matrix-adapter:8280"
as_token: <your-as-token>
hs_token: <your-hs-token>
sender_localpart: "00000000-0000-0000-0000-000000000000"  # Must match MATRIX_BOT_ACTOR_ID
namespaces:
  users:
    # Bot user - exclusive (security: prevents impersonation)
    - exclusive: true
      regex: "@00000000-0000-0000-0000-000000000000:.*"
    # Regular UUID users - NOT exclusive so they can login via OIDC/Element
    - exclusive: false
      regex: "@[0-9a-fA-F-]{36}:.*"
  aliases:
    - exclusive: true
      regex: "#[0-9a-fA-F-]{36}:.*"  # Room aliases
rate_limited: false

Important:

  • The sender_localpart must match the MATRIX_BOT_ACTOR_ID environment variable
  • The bot user namespace should be exclusive: true to prevent impersonation attacks

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages