Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
20e4387
fix: correct typo in members() query parameter
axel-krapotke Feb 16, 2026
bdcfcd0
feat: add injectable logger, replace console.* calls
axel-krapotke Feb 16, 2026
f11e694
feat(crypto): add CryptoManager with OlmMachine wrapper
axel-krapotke Feb 16, 2026
0cf1011
feat(http): add key upload/query/claim API methods
axel-krapotke Feb 16, 2026
dde28fc
feat(timeline): integrate E2EE decrypt in sync loop
axel-krapotke Feb 16, 2026
03d6918
feat(command): encrypt outgoing room events
axel-krapotke Feb 16, 2026
0f86f3d
feat(structure): add room encryption on creation
axel-krapotke Feb 16, 2026
10c8adf
feat(client): wire up CryptoManager in MatrixClient factory
axel-krapotke Feb 16, 2026
3207839
test(crypto): add smoke tests for CryptoManager
axel-krapotke Feb 16, 2026
a53010d
feat: add interactive playground CLI for testing
axel-krapotke Feb 16, 2026
66dfe27
refactor: make E2EE configurable per project/layer instead of globally
axel-krapotke Feb 16, 2026
118f68d
fix: preserve device_id in credentials, improve 401 token refresh han…
axel-krapotke Feb 16, 2026
ba5b983
fix: share CryptoManager across projectList/project calls
axel-krapotke Feb 16, 2026
d9586fe
fix: register room encryption state with OlmMachine
axel-krapotke Feb 16, 2026
49b496c
fix: explicit key query before sharing room keys
axel-krapotke Feb 16, 2026
a73b37f
debug: log to_device recipients to diagnose key sharing
axel-krapotke Feb 16, 2026
a7bb6ca
feat(playground): add 'send' command for plain m.room.message events
axel-krapotke Feb 16, 2026
9fd0b7d
test: add E2EE unit tests for CryptoManager
axel-krapotke Feb 16, 2026
06d608d
feat(crypto): add persistent IndexedDB-backed store support
axel-krapotke Mar 15, 2026
3ba45ed
test: add E2E integration tests against Tuwunel homeserver
axel-krapotke Mar 15, 2026
0001848
test: add matrix-client-api E2EE integration tests
axel-krapotke Mar 15, 2026
d6148ca
test: rewrite E2EE integration tests to use actual API layers
axel-krapotke Mar 15, 2026
5f31c02
feat(timeline): transparent crypto filter augmentation
axel-krapotke Mar 15, 2026
b7cc5f6
feat(factory): MatrixClient supports persistent crypto store
axel-krapotke Mar 15, 2026
efa0aaf
fix: include timeline state events in roomStateReducer
axel-krapotke Mar 16, 2026
d2509ba
fix: return encryption status from project join
axel-krapotke Mar 16, 2026
41b6d5a
fix: guard against missing timeline object in sync response
axel-krapotke Mar 16, 2026
92304f9
feat: share historical Megolm keys when new member joins encrypted room
axel-krapotke Mar 16, 2026
3161746
feat: share historical keys at layer share time (not just on join)
axel-krapotke Mar 16, 2026
ab77834
fix: schedule key sharing AFTER content posts via command queue
axel-krapotke Mar 16, 2026
fca0d7f
feat: expose state events in sync stream for self-join detection
axel-krapotke Mar 16, 2026
abaccf8
fix: historical key sharing via unencrypted custom to_device
axel-krapotke Mar 16, 2026
e0a7059
fix: Olm-encrypted historical key sharing (no plaintext leak)
axel-krapotke Mar 16, 2026
f2c5f81
cleanup: remove unused selfJoined emit from stream handler
axel-krapotke Mar 16, 2026
0a46d3f
fix: allow CONTRIBUTOR to send m.room.encrypted events
axel-krapotke Mar 16, 2026
79950e3
fix: include own events in content() for correct re-join state
axel-krapotke Mar 16, 2026
dea7d86
docs: rewrite README with full API documentation, E2EE details, and p…
axel-krapotke Mar 16, 2026
27b88f5
feat: SAS emoji device verification
axel-krapotke Mar 16, 2026
7366429
docs: add SAS device verification to README
axel-krapotke Mar 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules/
playground/
playground/.env
playground/.state.json
317 changes: 301 additions & 16 deletions Readme.md

Large diffs are not rendered by default.

123 changes: 94 additions & 29 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { CommandAPI } from './src/command-api.mjs'
import { ProjectList } from './src/project-list.mjs'
import { Project } from './src/project.mjs'
import { discover, errors } from './src/discover-api.mjs'
import { setLogger, LEVELS, consoleLogger, noopLogger } from './src/logger.mjs'
import { chill } from './src/convenience.mjs'
import { CryptoManager, VerificationMethod, VerificationRequestPhase } from './src/crypto.mjs'

/*
connect() resolves if the home_server can be connected. It does
Expand Down Expand Up @@ -36,45 +38,108 @@ const connect = (home_server_url) => async (controller) => {
* @property {String} user_id
* @property {String} password
* @property {String} home_server_url
* @property {Object} [encryption] - Optional encryption configuration
* @property {boolean} [encryption.enabled=false] - Enable E2EE
* @property {string} [encryption.storeName] - IndexedDB store name for persistent crypto state (e.g. 'crypto-<projectUUID>')
* @property {string} [encryption.passphrase] - Passphrase to encrypt the IndexedDB store
*
* @param {LoginData} loginData
* @returns {Object} matrixClient
*/
const MatrixClient = (loginData) => ({
const MatrixClient = (loginData) => {

connect: connect(loginData.home_server_url),

projectList: async mostRecentCredentials => {

const credentials = mostRecentCredentials ? mostRecentCredentials : (await HttpAPI.loginWithPassword(loginData))
const httpAPI = new HttpAPI(credentials)
const projectListParames = {
structureAPI: new StructureAPI(httpAPI),
timelineAPI: new TimelineAPI(httpAPI)
const encryption = loginData.encryption || null

// Shared CryptoManager instance – initialized once, reused across projectList/project calls
let sharedCryptoManager = null
let cryptoInitialized = false

/**
* Get or create the shared CryptoManager.
* If encryption.storeName is provided, uses IndexedDB-backed persistent store.
* Otherwise, uses in-memory store (keys lost on restart).
* @param {HttpAPI} httpAPI
* @returns {Promise<{cryptoManager: CryptoManager, httpAPI: HttpAPI} | null>}
*/
const getCrypto = async (httpAPI) => {
if (!encryption?.enabled) return null
if (sharedCryptoManager) {
// Reuse existing CryptoManager, just process any pending outgoing requests
if (!cryptoInitialized) {
await httpAPI.processOutgoingCryptoRequests(sharedCryptoManager)
cryptoInitialized = true
}
return { cryptoManager: sharedCryptoManager, httpAPI }
}
const projectList = new ProjectList(projectListParames)
projectList.tokenRefreshed = handler => httpAPI.tokenRefreshed(handler)
projectList.credentials = () => (httpAPI.credentials)
return projectList
},
const credentials = httpAPI.credentials
if (!credentials.device_id) {
throw new Error('E2EE requires a device_id in credentials. Ensure a fresh login (delete .state.json if reusing saved credentials).')
}
sharedCryptoManager = new CryptoManager()

project: async mostRecentCredentials => {
const credentials = mostRecentCredentials ? mostRecentCredentials : (await HttpAPI.loginWithPassword(loginData))
const httpAPI = new HttpAPI(credentials)
const projectParams = {
structureAPI: new StructureAPI(httpAPI),
timelineAPI: new TimelineAPI(httpAPI),
commandAPI: new CommandAPI(httpAPI)
if (encryption.storeName) {
// Persistent store: crypto state survives restarts (requires IndexedDB, i.e. Electron/browser)
await sharedCryptoManager.initializeWithStore(
credentials.user_id,
credentials.device_id,
encryption.storeName,
encryption.passphrase
)
} else {
// In-memory: keys are lost on restart (for testing or non-browser environments)
await sharedCryptoManager.initialize(credentials.user_id, credentials.device_id)
}
const project = new Project(projectParams)
project.tokenRefreshed = handler => httpAPI.tokenRefreshed(handler)
project.credentials = () => (httpAPI.credentials)
return project

await httpAPI.processOutgoingCryptoRequests(sharedCryptoManager)
cryptoInitialized = true
return { cryptoManager: sharedCryptoManager, httpAPI }
}
})

return {
connect: connect(loginData.home_server_url),

projectList: async mostRecentCredentials => {
const credentials = mostRecentCredentials ? mostRecentCredentials : (await HttpAPI.loginWithPassword(loginData))
const httpAPI = new HttpAPI(credentials)
const crypto = await getCrypto(httpAPI)
const projectListParames = {
structureAPI: new StructureAPI(httpAPI),
timelineAPI: new TimelineAPI(httpAPI, crypto)
}
const projectList = new ProjectList(projectListParames)
projectList.tokenRefreshed = handler => httpAPI.tokenRefreshed(handler)
projectList.credentials = () => (httpAPI.credentials)
if (crypto) projectList.cryptoManager = crypto.cryptoManager
return projectList
},

project: async mostRecentCredentials => {
const credentials = mostRecentCredentials ? mostRecentCredentials : (await HttpAPI.loginWithPassword(loginData))
const httpAPI = new HttpAPI(credentials)
const crypto = await getCrypto(httpAPI)
const projectParams = {
structureAPI: new StructureAPI(httpAPI),
timelineAPI: new TimelineAPI(httpAPI, crypto),
commandAPI: new CommandAPI(httpAPI, crypto?.cryptoManager || null),
cryptoManager: crypto?.cryptoManager || null
}
const project = new Project(projectParams)
project.tokenRefreshed = handler => httpAPI.tokenRefreshed(handler)
project.credentials = () => (httpAPI.credentials)
return project
}
}
}

export {
MatrixClient,
CryptoManager,
VerificationMethod,
VerificationRequestPhase,
connect,
discover
}
discover,
setLogger,
LEVELS,
consoleLogger,
noopLogger
}
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"description": "A minimal client API for [matrix]",
"main": "index.mjs",
"scripts": {
"test": "mocha ./test/*"
"test": "mocha ./test/*",
"test:e2e": "mocha --timeout 30000 ./test-e2e/*.test.mjs"
},
"keywords": [
"Matrix",
Expand All @@ -14,6 +15,7 @@
"author": "thomas.halwax@syncpoint.io",
"license": "MIT",
"dependencies": {
"@matrix-org/matrix-sdk-crypto-wasm": "^17.1.0",
"js-base64": "^3.7.7",
"ky": "^1.7.2"
},
Expand Down
6 changes: 6 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copy to .env and fill in your values
MATRIX_HOMESERVER=https://matrix.example.com
MATRIX_USER=@user:example.com
MATRIX_PASSWORD=your-password
# Set to "true" to enable E2EE (requires @matrix-org/matrix-sdk-crypto-wasm)
MATRIX_ENCRYPTION=false
47 changes: 47 additions & 0 deletions playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Playground

Interactive CLI to test the `matrix-client-api`.

## Setup

```bash
cd playground
cp .env.example .env
# Edit .env with your Matrix credentials
```

## Run

```bash
node cli.mjs
```

## Commands

Type `help` in the CLI for a full command list. Quick start:

```
login # Connect and authenticate
projects # List your projects
open <odin-id> # Open a specific project
layers # See layers in the project
layer-content <layer-id> # Fetch layer operations
listen # Stream live changes
stop # Stop streaming
loglevel 3 # Enable DEBUG logging
```

## E2EE

Set `MATRIX_ENCRYPTION=true` in `.env` to enable End-to-End Encryption.
Use `crypto-status` to check the OlmMachine state after login.

## Session Persistence

After login, credentials are saved to `.state.json` so you don't need to re-authenticate every time. Delete this file to force a fresh login.

## Notes

- This playground uses the library directly via relative import (`../index.mjs`)
- No `npm install` needed in the playground dir (dependencies come from the parent)
- The `.env` and `.state.json` files are gitignored
Loading