Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
47218d2
refactor: extract CryptoFacade, remove direct CryptoManager coupling
axel-krapotke Mar 19, 2026
5f7ff99
refactor: extract logger in CommandAPI.run() to single declaration
axel-krapotke Mar 19, 2026
b9ad297
fix: update E2E tests for crypto-facade API changes
axel-krapotke Mar 19, 2026
3add6fe
feat: room member cache to avoid per-message HTTP lookups
axel-krapotke Mar 19, 2026
6906c61
refactor: make getMemberIds a mandatory parameter of CommandAPI
axel-krapotke Mar 19, 2026
d232233
refactor: replace memberCache in Project with getMemberIds and onMemb…
axel-krapotke Mar 19, 2026
8c1f2b5
fix: clean up member cache on leaveLayer via onRoomLeft callback
axel-krapotke Mar 19, 2026
38d9d1a
refactor: replace callback trio with self-sufficient RoomMemberCache
axel-krapotke Mar 19, 2026
09fe80d
refactor: filter M_ROOM_MEMBER events once, use result for cache, han…
axel-krapotke Mar 19, 2026
fecb0c2
refactor: separate filter/map, cache update and stream handler into d…
axel-krapotke Mar 19, 2026
ec6c366
added home_server property
ThomasHalwax Mar 19, 2026
199cffe
refactor: pass RoomMemberCache directly to CommandAPI, remove getMemb…
axel-krapotke Mar 19, 2026
9fe742b
fix: handle network errors in RoomMemberCache.getMembers, fall back t…
axel-krapotke Mar 19, 2026
05036b9
Revert "fix: handle network errors in RoomMemberCache.getMembers, fal…
axel-krapotke Mar 19, 2026
1e3ebdc
fix: guard fetchMembers against network errors on cache miss
axel-krapotke Mar 19, 2026
b72f279
fix: e2e test that uses joinLayer needs to have a value for home_server
ThomasHalwax Mar 19, 2026
7eed9eb
Remove unused encryptionContent parameter from setRoomEncryption
axel-krapotke Mar 20, 2026
a157699
added oxlint
ThomasHalwax Mar 20, 2026
6a21f0a
removed unused imports and variables
ThomasHalwax Mar 20, 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
71 changes: 49 additions & 22 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ 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, TrustRequirement, VerificationMethod, VerificationRequestPhase } from './src/crypto.mjs'
import { CryptoFacade } from './src/crypto-facade.mjs'
import { RoomMemberCache } from './src/room-members.mjs'

/*
connect() resolves if the home_server can be connected. It does
Expand Down Expand Up @@ -43,69 +45,73 @@ const connect = (home_server_url) => async (controller) => {
* @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
* @property {Object} db - A levelup-compatible database instance for the persistent command queue
*
* @param {LoginData} loginData
*
* @param {LoginData} loginData
* @returns {Object} matrixClient
*/
const MatrixClient = (loginData) => {

const encryption = loginData.encryption || null

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

/**
* Get or create the shared CryptoManager.
* Get or create the shared CryptoFacade.
* 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>}
* @returns {Promise<CryptoFacade|null>}
*/
const getCrypto = async (httpAPI) => {
if (!encryption?.enabled) return null
if (sharedCryptoManager) {
// Reuse existing CryptoManager, just process any pending outgoing requests
if (sharedFacade) {
// Reuse existing facade, just process any pending outgoing requests
if (!cryptoInitialized) {
await httpAPI.processOutgoingCryptoRequests(sharedCryptoManager)
await sharedFacade.processOutgoingRequests()
cryptoInitialized = true
}
return { cryptoManager: sharedCryptoManager, httpAPI }
return sharedFacade
}
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()
const cryptoManager = new CryptoManager()

if (encryption.storeName) {
// Persistent store: crypto state survives restarts (requires IndexedDB, i.e. Electron/browser)
await sharedCryptoManager.initializeWithStore(
await cryptoManager.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)
await cryptoManager.initialize(credentials.user_id, credentials.device_id)
}

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

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 facade = await getCrypto(httpAPI)
const projectListParames = {
structureAPI: new StructureAPI(httpAPI),
timelineAPI: new TimelineAPI(httpAPI, crypto)
timelineAPI: new TimelineAPI(httpAPI, facade ? {
onSyncResponse: (data) => facade.processSyncResponse(data),
decryptEvent: (event, roomId) => facade.decryptEvent(event, roomId)
} : {})
}
const projectList = new ProjectList(projectListParames)
projectList.tokenRefreshed = handler => httpAPI.tokenRefreshed(handler)
Expand All @@ -116,12 +122,33 @@ const MatrixClient = (loginData) => {
project: async mostRecentCredentials => {
const credentials = mostRecentCredentials ? mostRecentCredentials : (await HttpAPI.loginWithPassword(loginData))
const httpAPI = new HttpAPI(credentials)
const crypto = await getCrypto(httpAPI)
const facade = await getCrypto(httpAPI)
const memberCache = new RoomMemberCache(async (roomId) => {
const members = await httpAPI.members(roomId)
return (members.chunk || [])
.filter(e => e.content?.membership === 'join')
.map(e => e.state_key)
.filter(Boolean)
})

const projectParams = {
structureAPI: new StructureAPI(httpAPI),
timelineAPI: new TimelineAPI(httpAPI, crypto),
commandAPI: new CommandAPI(httpAPI, crypto?.cryptoManager || null, loginData.db),
cryptoManager: crypto?.cryptoManager || null
timelineAPI: new TimelineAPI(httpAPI, facade ? {
onSyncResponse: (data) => facade.processSyncResponse(data),
decryptEvent: (event, roomId) => facade.decryptEvent(event, roomId)
} : {}),
commandAPI: new CommandAPI(httpAPI, memberCache, {
encryptEvent: facade
? (roomId, type, content, memberIds) => facade.encryptEvent(roomId, type, content, memberIds)
: null,
db: loginData.db
}),
memberCache,
crypto: facade ? {
isEnabled: true,
registerRoom: (roomId, enc) => facade.registerRoom(roomId, enc),
shareHistoricalKeys: (roomId, userIds) => facade.shareHistoricalKeys(roomId, userIds)
} : { isEnabled: false }
}
const project = new Project(projectParams)
project.tokenRefreshed = handler => httpAPI.tokenRefreshed(handler)
Expand Down
Loading